Encapsulamiento en Java

📅 Actualizado en marzo 2026 ✍️ Ángel López ⏱️ 15 min de lectura ✓ Nivel intermedio

🛡️ ¿Qué es el encapsulamiento?

El encapsulamiento (encapsulation) es uno de los cuatro pilares fundamentales de la programación orientada a objetos, junto con la abstracción, la herencia y el polimorfismo. Consiste en agrupar los datos (atributos) y los métodos (comportamiento) que operan sobre esos datos dentro de una misma unidad —la clase— y restringir el acceso directo a los datos internos desde el exterior.

La idea central es sencilla: los campos de una clase se declaran como private y se proporcionan métodos públicos (getters y setters) que actúan como único punto de entrada controlado. De este modo, la clase puede validar, transformar o rechazar cualquier valor antes de almacenarlo, garantizando la integridad del estado del objeto en todo momento.

💡
Concepto clave: Encapsular no es simplemente «poner private»; es diseñar una interfaz pública estable que oculte los detalles internos, de modo que el resto del programa dependa del qué hace la clase y no del cómo lo hace.

Un ejemplo cotidiano ayuda a visualizarlo: un cajero automático permite retirar dinero, consultar saldo o transferir fondos a través de una pantalla (la interfaz pública), pero nunca expone directamente los mecanismos internos que mueven los billetes ni la base de datos del banco. El usuario interactúa con operaciones controladas, y el sistema protege su integridad internamente.

// Ejemplo básico de encapsulamiento
public class CuentaBancaria {

    private String titular;
    private double saldo;

    public CuentaBancaria(String titular, double saldoInicial) {
        this.titular = titular;
        this.saldo = saldoInicial;
    }

    public double getSaldo() {
        return saldo;
    }

    public void depositar(double cantidad) {
        if (cantidad > 0) {
            saldo += cantidad;
        }
    }

    public boolean retirar(double cantidad) {
        if (cantidad > 0 && cantidad <= saldo) {
            saldo -= cantidad;
            return true;
        }
        return false;
    }
}

En este ejemplo, nadie puede asignar un saldo negativo directamente (cuenta.saldo = -500 no compila porque saldo es private). Toda interacción pasa por los métodos depositar() y retirar(), que contienen la lógica de protección.

🎯 ¿Por qué importa el encapsulamiento?

Aplicar encapsulamiento de forma disciplinada aporta una serie de ventajas que se hacen evidentes a medida que un proyecto crece en tamaño y complejidad:

🔹 Integridad de los datos

Al controlar el acceso a los campos mediante métodos, se impide que el objeto alcance un estado inválido. Por ejemplo, una clase Empleado puede rechazar un salario negativo o una edad fuera de rango antes de almacenar el valor.

🔹 Bajo acoplamiento entre clases

Si otras clases dependen solo de los métodos públicos y no de los campos internos, es posible cambiar la representación interna sin romper el código que utiliza la clase. Este principio se conoce como programar contra interfaces, no contra implementaciones.

🔹 Facilidad de mantenimiento

Un campo privado con getter y setter centraliza la lógica de lectura y escritura en un solo punto. Si más adelante se necesita registrar cada cambio en un log, basta con modificar el setter; no hace falta buscar y corregir cientos de asignaciones directas dispersas por el proyecto.

🔹 Reutilización y extensibilidad

Las clases bien encapsuladas son componentes autónomos que pueden reutilizarse en otros proyectos o extenderse mediante herencia sin temor a efectos secundarios imprevistos.

Buena práctica: Declara siempre los campos de instancia como private por defecto. Solo amplia la visibilidad (protected, public) cuando exista una razón justificada y documentada.

🔐 Modificadores de acceso en Java

Java proporciona cuatro niveles de visibilidad que determinan desde dónde se puede acceder a un miembro (campo o método) de una clase. Conocerlos a fondo es imprescindible para aplicar correctamente el encapsulamiento.

Modificador Misma clase Mismo paquete Subclase (otro paquete) Cualquier clase
private
(default) — sin modificador
protected
public

▶️ El modificador private

Es el nivel más restrictivo. Un miembro private solo es visible dentro de la propia clase. Este es el modificador que se aplica por convención a todos los campos de instancia para lograr el encapsulamiento.

public class Producto {

    private String nombre;
    private double precio;

    public String getNombre() {
        return nombre;
    }

    public void setPrecio(double precio) {
        if (precio >= 0) {
            this.precio = precio;
        }
    }
}

▶️ Acceso default (paquete)

Cuando no se especifica ningún modificador, el miembro es accesible desde cualquier clase del mismo paquete. Es útil para clases auxiliares internas que colaboran estrechamente entre sí, pero no es recomendable para campos de instancia porque rompe el encapsulamiento dentro del paquete.

▶️ El modificador protected

Permite el acceso desde el mismo paquete y desde subclases aunque estén en paquetes distintos. Se utiliza cuando una clase padre necesita que sus hijas accedan a ciertos campos sin exponerlos al público general.

▶️ El modificador public

El nivel más abierto: el miembro es accesible desde cualquier lugar. Se utiliza para los métodos que conforman la interfaz pública de la clase (getters, setters, métodos de negocio). Nunca se debe aplicar a campos de instancia salvo en casos excepcionales como constantes (public static final).

⚠️
Error frecuente: Declarar campos como public «para ir más rápido» durante el desarrollo. Este atajo genera deuda técnica que se paga con intereses: cuando el proyecto crece, resulta costoso y arriesgado cambiar la visibilidad porque ya hay decenas de clases accediendo directamente al campo.

🔄 Getters y setters

Los métodos getter y setter (también llamados métodos de acceso y métodos de modificación, respectivamente) constituyen el mecanismo estándar para interactuar con los campos encapsulados de un objeto.

🔹 El getter: obtener el valor

Un getter devuelve el valor actual de un campo privado. Su firma habitual es:

public TipoDelCampo getNombreDelCampo() {
    return nombreDelCampo;
}

Si el campo es de tipo boolean, la convención Java es utilizar el prefijo is en lugar de get:

private boolean activo;

public boolean isActivo() {
    return activo;
}

🔹 El setter: modificar el valor

Un setter recibe un parámetro y lo asigna al campo privado tras aplicar, opcionalmente, una validación:

public void setNombreDelCampo(TipoDelCampo nuevoValor) {
    this.nombreDelCampo = nuevoValor;
}

La palabra clave this diferencia el campo de instancia del parámetro cuando ambos comparten nombre, como es habitual en los setters.

🔹 Ejemplo completo con getter y setter

public class Estudiante {

    private String nombre;
    private int edad;

    // Getter de nombre
    public String getNombre() {
        return nombre;
    }

    // Setter de nombre
    public void setNombre(String nombre) {
        if (nombre != null && !nombre.isBlank()) {
            this.nombre = nombre;
        }
    }

    // Getter de edad
    public int getEdad() {
        return edad;
    }

    // Setter de edad con validación
    public void setEdad(int edad) {
        if (edad >= 0 && edad <= 130) {
            this.edad = edad;
        }
    }
}

Observemos cómo los setters no aceptan valores nulos ni fuera de rango. Gracias al encapsulamiento, ningún código externo puede violar estas restricciones.

✅ Validación en los métodos de acceso

El verdadero valor del encapsulamiento se materializa cuando los setters no se limitan a asignar, sino que validan y protegen. A continuación se presentan distintas estrategias de validación que se pueden combinar según las necesidades del diseño:

▶️ Validación silenciosa (ignorar el valor inválido)

El setter simplemente no modifica el campo si el valor no es válido. Es el enfoque más sencillo pero puede ocultar errores, ya que el código que llama al setter no recibe ninguna señal de que la asignación no se produjo.

public void setPrecio(double precio) {
    if (precio >= 0) {
        this.precio = precio;
    }
    // Si precio < 0, no se hace nada
}

▶️ Validación con excepción

El setter lanza una excepción cuando el valor es inválido, obligando al código llamante a tratar el error. Es el enfoque recomendado en la mayoría de aplicaciones profesionales.

public void setPrecio(double precio) {
    if (precio < 0) {
        throw new IllegalArgumentException(
            "El precio no puede ser negativo: " + precio
        );
    }
    this.precio = precio;
}

▶️ Validación en el constructor

Es fundamental validar también en el constructor, de modo que el objeto nunca exista en un estado inválido. La mejor práctica consiste en reutilizar los propios setters dentro del constructor:

public class Producto {

    private String nombre;
    private double precio;

    public Producto(String nombre, double precio) {
        setNombre(nombre);  // Reutiliza la validación
        setPrecio(precio);  // Reutiliza la validación
    }

    public void setNombre(String nombre) {
        if (nombre == null || nombre.isBlank()) {
            throw new IllegalArgumentException("El nombre no puede estar vacío");
        }
        this.nombre = nombre;
    }

    public void setPrecio(double precio) {
        if (precio < 0) {
            throw new IllegalArgumentException("Precio negativo no permitido");
        }
        this.precio = precio;
    }

    public String getNombre() { return nombre; }
    public double getPrecio() { return precio; }
}
Buena práctica: Llamar a los setters desde el constructor garantiza que la lógica de validación está centralizada en un solo punto. Si mañana cambian las reglas de negocio, solo hay que tocar el setter.

🧊 Encapsulamiento e inmutabilidad

Llevar el encapsulamiento a su máxima expresión conduce al concepto de objetos inmutables: una vez creados, su estado no puede cambiar. Las clases inmutables son inherentemente seguras en entornos multihilo (thread-safe) y eliminan toda una categoría de errores relacionados con modificaciones inesperadas.

Para crear una clase inmutable en Java se deben cumplir estas reglas:

Regla Descripción
Declarar la clase como final Impide que una subclase sobreescriba métodos y rompa la inmutabilidad.
Campos private final El compilador asegura que cada campo se asigne una sola vez.
No proporcionar setters Sin setters, no existe forma de modificar el estado después de la construcción.
Copiar objetos mutables Si el constructor recibe un objeto mutable (lista, fecha...), almacenar una copia defensiva.
public final class Coordenada {

    private final double latitud;
    private final double longitud;

    public Coordenada(double latitud, double longitud) {
        this.latitud = latitud;
        this.longitud = longitud;
    }

    public double getLatitud()  { return latitud; }
    public double getLongitud() { return longitud; }

    // Sin setters: el objeto es inmutable

    @Override
    public String toString() {
        return "(" + latitud + ", " + longitud + ")";
    }
}
📘
Nota: La clase String de Java es el ejemplo más conocido de clase inmutable. Cada operación como concat() o toUpperCase() devuelve un nuevo objeto String; el original nunca se modifica.

☕ Convención JavaBeans

La especificación JavaBeans establece un conjunto de convenciones de nomenclatura que los frameworks de Java (Spring, Hibernate, JSP, Jackson, entre otros) utilizan para acceder a las propiedades de los objetos de manera automática mediante reflexión. Comprender estas convenciones es esencial para cualquier desarrollador Java.

🔹 Reglas de nomenclatura

Tipo de campo Getter Setter
Cualquier tipo (no booleano) getTipo() setTipo(valor)
boolean isTipo() setTipo(valor)

🔹 Ejemplo de JavaBean completo

import java.io.Serializable;

public class Cliente implements Serializable {

    private String nombre;
    private String email;
    private boolean activo;

    // Constructor sin argumentos (requerido por JavaBeans)
    public Cliente() {}

    public String getNombre()  { return nombre; }
    public void setNombre(String nombre)  { this.nombre = nombre; }

    public String getEmail()   { return email; }
    public void setEmail(String email)    { this.email = email; }

    public boolean isActivo() { return activo; }
    public void setActivo(boolean activo) { this.activo = activo; }
}

Las tres características de un JavaBean son: constructor sin argumentos, campos privados con getters y setters siguiendo la convención de nombres, y la implementación de Serializable (esto último no siempre es obligatorio, pero sí recomendado para persistencia y comunicación remota).

🏗️ Ejemplo completo integrador

El siguiente programa simula un sistema de gestión de empleados de una empresa. Aplica encapsulamiento en todas las clases, validación en los setters, y demuestra cómo la interfaz pública protege la integridad de los datos.

public class Empleado {

    private String nombre;
    private String departamento;
    private double salario;
    private boolean activo;

    public Empleado(String nombre, String departamento, double salario) {
        setNombre(nombre);
        setDepartamento(departamento);
        setSalario(salario);
        this.activo = true;
    }

    // --- Getters ---
    public String  getNombre()       { return nombre; }
    public String  getDepartamento() { return departamento; }
    public double  getSalario()      { return salario; }
    public boolean isActivo()       { return activo; }

    // --- Setters con validación ---
    public void setNombre(String nombre) {
        if (nombre == null || nombre.isBlank()) {
            throw new IllegalArgumentException("El nombre no puede estar vacío");
        }
        this.nombre = nombre.trim();
    }

    public void setDepartamento(String departamento) {
        if (departamento == null || departamento.isBlank()) {
            throw new IllegalArgumentException("El departamento no puede estar vacío");
        }
        this.departamento = departamento.trim();
    }

    public void setSalario(double salario) {
        if (salario < 0) {
            throw new IllegalArgumentException(
                "El salario no puede ser negativo: " + salario
            );
        }
        this.salario = salario;
    }

    // --- Métodos de negocio ---
    public void aplicarAumento(double porcentaje) {
        if (porcentaje <= 0 || porcentaje > 100) {
            throw new IllegalArgumentException(
                "Porcentaje inválido: " + porcentaje
            );
        }
        this.salario += this.salario * (porcentaje / 100);
    }

    public void desactivar() {
        this.activo = false;
    }

    @Override
    public String toString() {
        return "Empleado{" +
               "nombre='" + nombre + '\'' +
               ", depto='" + departamento + '\'' +
               ", salario=" + String.format("%.2f", salario) +
               ", activo=" + activo +
               '}';
    }
}
public class SistemaEmpleados {

    public static void main(String[] args) {

        // Crear empleados con datos válidos
        Empleado e1 = new Empleado("Ana García", "Desarrollo", 45000);
        Empleado e2 = new Empleado("Carlos López", "Marketing", 38000);

        System.out.println("=== Estado inicial ===");
        System.out.println(e1);
        System.out.println(e2);

        // Aplicar aumento del 10%
        e1.aplicarAumento(10);
        System.out.println("\n=== Tras aumento del 10% a Ana ===");
        System.out.println(e1);

        // Intentar asignar salario negativo
        try {
            e2.setSalario(-5000);
        } catch (IllegalArgumentException ex) {
            System.out.println("\n=== Error controlado ===");
            System.out.println(ex.getMessage());
        }

        // Desactivar empleado
        e2.desactivar();
        System.out.println("\n=== Carlos desactivado ===");
        System.out.println(e2);
    }
}

// Salida esperada:
// === Estado inicial ===
// Empleado{nombre='Ana García', depto='Desarrollo', salario=45000.00, activo=true}
// Empleado{nombre='Carlos López', depto='Marketing', salario=38000.00, activo=true}
//
// === Tras aumento del 10% a Ana ===
// Empleado{nombre='Ana García', depto='Desarrollo', salario=49500.00, activo=true}
//
// === Error controlado ===
// El salario no puede ser negativo: -5000.0
//
// === Carlos desactivado ===
// Empleado{nombre='Carlos López', depto='Marketing', salario=38000.00, activo=false}

🐛 Errores frecuentes

⚠️
Error 1: Declarar campos como public
Exponer directamente los campos permite que cualquier clase modifique el estado sin validación, generando errores difíciles de rastrear.
// ❌ INCORRECTO
public class Rectangulo {
    public double ancho;   // Accesible sin control
    public double alto;    // Accesible sin control
}

// Cualquier código externo puede hacer:
Rectangulo r = new Rectangulo();
r.ancho = -50;  // Estado inválido, sin protección
// ✅ CORRECTO
public class Rectangulo {
    private double ancho;
    private double alto;

    public void setAncho(double ancho) {
        if (ancho > 0) this.ancho = ancho;
    }

    public void setAlto(double alto) {
        if (alto > 0) this.alto = alto;
    }

    public double getAncho() { return ancho; }
    public double getAlto()  { return alto; }
}
⚠️
Error 2: Getters y setters sin lógica (encapsulamiento superficial)
Crear getters y setters que no hacen más que devolver y asignar es equivalente a declarar el campo como público. Si no hay validación ni transformación, hay que plantearse si realmente se necesita ese setter.
⚠️
Error 3: Exponer referencias mutables desde un getter
Si un campo es una lista o un array, devolver la referencia directa permite al código externo modificar la colección sin pasar por la clase. La solución es devolver una copia defensiva:
// ❌ INCORRECTO — expone la lista interna
public List<String> getNotas() {
    return notas;  // El llamante puede hacer getNotas().clear()
}

// ✅ CORRECTO — devuelve copia defensiva
public List<String> getNotas() {
    return new ArrayList<>(notas);  // Copia independiente
}

// ✅ ALTERNATIVA — devuelve vista no modificable
public List<String> getNotas() {
    return Collections.unmodifiableList(notas);
}
⚠️
Error 4: Olvidar validar en el constructor
Si los setters validan pero el constructor asigna directamente los campos con this.campo = valor, se puede crear un objeto en estado inválido. Siempre reutilizar los setters dentro del constructor.

📝 Ejercicios prácticos

Ejercicio 1: Comprensión — ¿Qué imprime este código?

Analiza el siguiente código y determina la salida por consola sin ejecutarlo:

public class Termometro {
    private double temperatura;

    public void setTemperatura(double temperatura) {
        if (temperatura >= -273.15) {
            this.temperatura = temperatura;
        }
    }

    public double getTemperatura() {
        return temperatura;
    }

    public static void main(String[] args) {
        Termometro t = new Termometro();
        t.setTemperatura(36.5);
        System.out.println(t.getTemperatura());
        t.setTemperatura(-300);
        System.out.println(t.getTemperatura());
    }
}

Ejercicio 2: Aplicación — Clase CuentaAhorro

Escribe una clase CuentaAhorro con los siguientes requisitos:

  • Campos privados: titular (String), saldo (double) y tasaInteres (double, entre 0 y 15).
  • Constructor que valide todos los campos.
  • Método aplicarInteres() que sume al saldo el interés anual correspondiente.
  • Método retirar(double cantidad) que solo permita retirar si hay saldo suficiente.

Ejercicio 3: Diseño — Clase inmutable Libro

Diseña una clase inmutable llamada Libro con los campos titulo (String), autor (String) e isbn (String). La clase debe cumplir todas las reglas de inmutabilidad explicadas en este artículo. Añade también un método toString() descriptivo.

❓ Preguntas frecuentes sobre Encapsulamiento en Java

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

La abstracción consiste en mostrar solo lo esencial de un objeto al exterior, ocultando los detalles de implementación. El encapsulamiento es el mecanismo concreto que lo hace posible: agrupa datos y métodos en una clase y restringe el acceso directo a los datos mediante modificadores de acceso. Dicho de otro modo, la abstracción es el concepto y el encapsulamiento es la técnica que lo implementa.
Si una variable de instancia es pública, cualquier parte del programa puede modificarla sin control, lo que provoca estados inválidos, errores difíciles de rastrear y una fuerte dependencia entre clases. Al declararlas privadas y proporcionar getters y setters, se centraliza la validación, se facilita el mantenimiento y se pueden cambiar los detalles internos sin afectar al código que usa la clase.
No. Solo se deben crear los métodos de acceso que realmente necesite la clase. Exponer todos los campos con getters y setters automáticos equivale en la práctica a hacerlos públicos. Es preferible exponer solo lo estrictamente necesario y, cuando sea posible, diseñar clases inmutables que solo ofrezcan getters.
El principio de ocultación de información (information hiding), propuesto por David Parnas, establece que cada módulo debe ocultar sus decisiones de diseño internas. El encapsulamiento en Java materializa este principio: los campos privados ocultan la representación interna y los métodos públicos constituyen la interfaz estable que el resto del programa utiliza.
La convención JavaBeans establece reglas para nombrar getters y setters: getPropiedad() para obtener el valor, setPropiedad(valor) para asignarlo, e isPropiedad() para booleanos. Esta convención facilita que los frameworks de Java (Spring, Hibernate, JSP) accedan a las propiedades de los objetos de forma estandarizada, y solo funciona correctamente cuando los campos están encapsulados con acceso privado.
Valora este artículo

💬 Foro de discusión

¿Tienes dudas sobre Encapsulamiento en Java? Comparte tu pregunta con la comunidad.

¿Tienes cuenta? o comenta como invitado ↓

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