W3docs

Encapsulamiento en Java

Agrupa datos y métodos en clases Java y oculta detalles de implementación usando campos privados y accesores públicos.

El encapsulamiento es el primero de los cuatro pilares de la POO, y el más fácil de aplicar en la práctica: mantén los datos de una clase como privados y expón el comportamiento a través de métodos. La clase se vuelve responsable de su propio estado — ningún código externo puede dejarlo en un estado inválido — y el resto del programa se comunica con ella a través de una interfaz que tú controlas.

El mecanismo son campos private más métodos public. La disciplina es "no expongas estado que no puedas controlar."

El patrón

Una clase sin encapsulamiento es simplemente un conjunto de campos públicos:

public class Account {
  public int balance;
}

Account a = new Account();
a.balance = 100;
a.balance = -50;   // nothing stops this

La versión encapsulada oculta el campo y expone operaciones deliberadas:

public class Account {
  private int balance;

  public int  balance()              { return balance; }
  public void deposit(int amount) {
    if (amount <= 0) throw new IllegalArgumentException();
    balance += amount;
  }
  public boolean withdraw(int amount) {
    if (amount <= 0) throw new IllegalArgumentException();
    if (amount > balance) return false;
    balance -= amount;
    return true;
  }
}

Ahora no hay forma de que el código externo establezca balance en un número negativo, lo sobreescriba sin pasar por la validación, o lo consulte sin pasar por balance().

Lo que ganas

Invariantes en los que puedes confiar. La clase Account impone que "el saldo ≥ 0 después de cada operación pública." Como nada externo puede tocar balance, la regla se puede aplicar desde un solo archivo, no desde toda la base de código.

Libertad para cambiar. Con balance privado, puedes cambiar más adelante su tipo de int a long, cambiar la unidad de dólares a centavos, almacenarlo en un BigDecimal, o moverlo a una base de datos — sin tocar ni un solo llamador. Con un campo público, el tipo y el almacenamiento quedan grabados en cada punto de llamada.

Una API más pequeña y clara. Los llamadores solo ven deposit, withdraw, balance — no la docena de métodos auxiliares privados que los hacen funcionar. La clase anuncia lo que hace, no cómo.

Localidad del razonamiento. Cuando algo falla con balance, el error está en uno de tres métodos, no en cualquiera de los miles de lugares que de otro modo podrían escribir el campo.

Encapsulamiento ≠ getters y setters para todo

Un antipatrón común es generar mecánicamente un getter y un setter para cada campo:

public class Account {
  private int balance;
  public int  getBalance()           { return balance; }
  public void setBalance(int v)      { this.balance = v; }    // same as public field, with extra steps
}

Esto está técnicamente "encapsulado" en el sentido del libro de texto, pero no logra ninguno de los beneficios reales del encapsulamiento — cualquiera puede seguir poniendo el objeto en el estado que desee. El encapsulamiento real expresa operaciones, no acceso directo a campos:

  • deposit(amount) en lugar de setBalance(balance + amount)
  • withdraw(amount) que devuelve éxito/fracaso en lugar de setBalance(balance - amount)
  • balance() (un accesor de solo lectura) sin un setBalance correspondiente

El siguiente capítulo sobre getters y setters repasa las convenciones sobre cuándo es apropiado cada uno.

Copias defensivas

Si el tipo de un campo es mutable (un array, una lista, un Date), devolverlo directamente filtra el control:

public class Order {
  private final List<String> items = new ArrayList<>();
  public List<String> items() { return items; }      // leak!
}

Order o = new Order();
o.items().add("apple");                              // outside code mutated the order

La solución es devolver una vista no modificable o una copia defensiva:

public List<String> items() {
  return List.copyOf(items);          // immutable snapshot
}

El mismo cuidado aplica a los setters que reciben valores mutables — cópialos al entrar:

public Order(List<String> items) {
  this.items = new ArrayList<>(items);
}

El capítulo sobre clases inmutables profundiza en este tema.

Elegir el nivel de acceso correcto

private y public son los dos que usas con más frecuencia, pero Java tiene cuatro niveles, y los intermedios importan para el encapsulamiento:

  • private — visible solo dentro de la misma clase. El valor predeterminado para campos y métodos auxiliares.
  • package-private (sin palabra clave) — visible para otras clases del mismo paquete. Útil cuando varias clases cooperantes forman una unidad y deben ver los detalles internos de las demás, pero el mundo exterior no debería.
  • protected — package-private más visible para las subclases. Resérvalo para miembros que una subclase realmente necesita sobreescribir o extender.
  • public — visible en todos lados. Esta es tu API publicada; una vez que el código externo depende de ella, cambiarla rompe a los llamadores.

La regla general es el principio de mínima exposición: da a cada miembro el acceso más restringido que aún permita que el código funcione, y amplíalo solo cuando aparezca una necesidad concreta. Un campo o método public es una promesa que debes mantener. Consulta el capítulo sobre modificadores de acceso para la tabla completa.

El encapsulamiento en el diseño

A partir de cierto tamaño, el encapsulamiento se convierte en una herramienta de diseño, no solo en una regla de codificación. Cada clase traza un límite alrededor de un conjunto de estados y las operaciones sobre ellos; el resto del sistema se comunica con ella a través de ese límite. La mayoría de los patrones arquitectónicos — en capas, hexagonal, MVC, Clean — son argumentos sobre dónde trazar los límites.

Pautas prácticas:

  • Los campos son privados hasta que se demuestre lo contrario. Empieza así; amplía solo con una razón.
  • Expón verbos, no sustantivos. cancel(), pay(), ship() son mejores que setStatus(...).
  • No devuelvas internos mutables sin procesar. Envuelve, copia o usa una vista inmutable.
  • Valida al entrar. Rechaza el estado imposible en el límite para que el código interno pueda asumir que es válido.

Un ejemplo elaborado

java— editable, runs on the server

Qué sigue

La parte mecánica del encapsulamiento — los campos private con métodos public que los leen o escriben — tiene sus propias convenciones que vale la pena conocer. Continúa con getters y setters.

Práctica

Práctica
¿Por qué generar mecánicamente un getter público y un setter público para cada campo privado anula el propósito del encapsulamiento?
¿Por qué generar mecánicamente un getter público y un setter público para cada campo privado anula el propósito del encapsulamiento?
Was this page helpful?