Encapsulación: agrupar datos y proteger el estado

Hasta ahora escribimos un programa en estilo procedural, donde teníamos variables sueltas dentro del método main y un método estático que recibía esos valores como parámetros:

public class Main {
    public static void main(String[] args) {

        int salarioBase = 50_000;
        int horaExtra = 10;
        int tasaHoraria = 20;

        int salario = salario(salarioBase, horaExtra, tasaHoraria);
        System.out.println("Salario: " + salario);
    }

    public static int salario(int salarioBase, int horaExtra, int tasaHoraria) {
        return salarioBase + (horaExtra * tasaHoraria);
    }
}

Funciona, sí. Pero ahora vamos a aplicar el primer principio importante de la POO: la encapsulación.


¿Qué es encapsulación?

Encapsulación significa agrupar:

  • los datos (atributos), y
  • los métodos (comportamientos) que operan sobre esos datos

dentro de una sola unidad: un objeto.

En el código procedural, los datos estaban repartidos en variables locales:

  • salarioBase
  • horaExtra
  • tasaHoraria

y el cálculo estaba en un método aparte que dependía de que siempre le pasáramos esos valores correctamente.

La idea de encapsulación es: en lugar de tener datos por un lado y funciones por otro, los metemos en una misma “cápsula”.


Crear la clase Empleado

Como estamos calculando el salario de un empleado, esa “cápsula” tiene un nombre natural: Empleado.

Creamos una nueva clase llamada Empleado y movemos allí los datos como campos (fields):

public class Empleado {
    public int salarioBase;
    public int tasaHoraria;

    public int calcularSalario(int horaExtra) {
        return salarioBase + (tasaHoraria * horaExtra);
    }
}

Fíjate en lo que cambió:

Antes el método tenía tres parámetros. Ahora el método tiene solo uno (horaExtra), porque:

  • salarioBase y tasaHoraria ya viven dentro del objeto (como fields).
  • y horaExtra puede variar cada mes, así que tiene sentido pasarlo como argumento cada vez.

Este es un primer indicio de diseño orientado a objetos: no estamos cargando el método con un montón de parámetros, porque parte de la información ya pertenece al objeto.


Usar la clase desde Main

Ahora en Main creamos un objeto Empleado, le asignamos datos y usamos su método:

public class Main {
    public static void main(String[] args) {

        var empleado = new Empleado();
        empleado.salarioBase = 50_000;
        empleado.tasaHoraria = 20;

        int salario = empleado.calcularSalario(10);
        System.out.println("Salario: " + salario);
    }
}

Con este cambio:

  • el main queda más limpio,
  • el cálculo está en el lugar correcto (en Empleado),
  • y si mañana quieres calcular salarios en otro proyecto, puedes reutilizar la clase Empleado.

Hasta aquí todo se ve hermoso… pero ahora aparece un problema importante.


El problema: el objeto puede quedar en un estado inválido

Como los campos son public, cualquiera puede hacer esto:

empleado.salarioBase = -1;

Eso pone al objeto en un estado malo. Y un objeto en estado inválido puede producir resultados incorrectos.

Podrías decir: “pero nadie debería poner un salario negativo”. De acuerdo. Pero, ¿y si el valor viene de un formulario, una base de datos o la entrada del usuario?

Aquí necesitamos validación.


La solución correcta: ocultar los campos y controlar el acceso

No queremos escribir validaciones por todas partes, así:

if (salarioBase < 0) ...

porque eso implica repetir la misma lógica cada vez que usemos Empleado. En POO la idea es: la validación debe vivir dentro de la clase, para que se aplique siempre.

Para lograrlo:

  1. Cambiamos el campo a private (privado).
  2. Creamos un método para establecer el valor con validación.

Eso se hace con un setter.


private + setter: proteger el estado

Mira cómo queda la clase:

public class Empleado {
    private int salarioBase;   // ahora está protegido
    public int tasaHoraria;    // este lo dejamos público por ahora

    public int calcularSalario(int horaExtra) {
        return salarioBase + (tasaHoraria * horaExtra);
    }

    public void setSalarioBase(int salarioBase) {
        if (salarioBase <= 0)
            throw new IllegalArgumentException("Salario no puede ser menor o igual a 0");

        this.salarioBase = salarioBase;
    }
}

¿Qué está pasando aquí?

  • private int salarioBase; significa: nadie fuera de la clase puede acceder directamente a este campo.
  • La única forma correcta de asignarlo ahora es usando:
setSalarioBase(...)

Y ahí adentro hacemos la validación.


¿Qué es throw y qué es IllegalArgumentException?

En Java, throw significa lanzar una excepción.

Una excepción es una forma de decirle al programa:

“Algo está mal, y no puedo continuar normalmente”.

IllegalArgumentException es una excepción estándar de Java que se usa cuando alguien pasa un argumento inválido.

Entonces, si alguien intenta hacer:

empleado.setSalarioBase(-1);

el programa detiene su ejecución y muestra un error como:

  • IllegalArgumentException: Salario no puede ser menor o igual a 0

Esto es útil porque evita que el objeto quede en un estado imposible.


¿Y si quiero leer el salario base?

Ahora que salarioBase es privado, esto ya no funciona:

System.out.println(empleado.salarioBase); // ❌ error

Porque el campo está oculto. Entonces necesitamos una forma controlada de leerlo: un getter.

public int getSalarioBase() {
    return salarioBase;
}

Los getters y setters son métodos comunes en Java:

  • setter: asigna un valor con control/validación.
  • getter: devuelve el valor de un campo privado.

Clase completa: encapsulación real

Aquí está la clase Empleado ya bien encapsulada:

public class Empleado {
    private int salarioBase;
    private int tasaHoraria;   // también lo protegemos
    public int horaExtra;      // lo dejamos público por ahora

    public int calcularSalario(int horaExtra) {
        return salarioBase + (tasaHoraria * horaExtra);
    }

    public void setSalarioBase(int salarioBase) {
        if (salarioBase <= 0)
            throw new IllegalArgumentException("Salario no puede ser menor o igual a 0");
        this.salarioBase = salarioBase;
    }

    public int getSalarioBase() {
        return salarioBase;
    }

    public void setTasaHoraria(int tasaHoraria) {
        if (tasaHoraria < 0)
            throw new IllegalArgumentException("La tarifa por hora no puede ser negativa");
        this.tasaHoraria = tasaHoraria;
    }

    public int getTasaHoraria() {
        return tasaHoraria;
    }
}

Probarlo desde Main

public class Main {
    public static void main(String[] args) {

        Empleado e = new Empleado();

        // Asignamos valores con setters (con validación)
        e.setSalarioBase(1000);
        e.setTasaHoraria(50);

        // Leemos valores con getters
        System.out.println("Salario base: " + e.getSalarioBase());
        System.out.println("Tarifa por hora: " + e.getTasaHoraria());

        // Calculamos con 10 horas extra
        int total = e.calcularSalario(10);
        System.out.println("Salario con horas extra: " + total);
    }
}

Si el salario base es 1000, la tarifa por hora es 50, y trabajó 10 horas extra, entonces:

[ 1000 + (50 ) = 1500]

Así que imprime:

  • Salario base: 1000
  • Tarifa por hora: 50
  • Salario con horas extra: 1500

¿Qué ganamos con esto?

Con encapsulación logramos tres cosas muy importantes:

Primero, organización: los datos y el cálculo están en la misma clase (Empleado). Segundo, menos errores: no permitimos que el objeto quede en estados inválidos (salario negativo, tarifa negativa). Tercero, reutilización: esta clase se puede mover a otro proyecto o a una biblioteca y seguir funcionando igual, con sus validaciones incluidas.

En Java, la regla general es:

Campos privados + getters y setters (cuando tenga sentido).

Getters y setters

Partimos de este código en Main, donde creamos un objeto empleado y modificamos directamente sus campos:

public class Main {
    public static void main(String[] args) {

        var empleado = new empleado();
        empleado.salarioBase = 50_000;
        empleado.tasaHoraria = 20;

        int salario = empleado.calcularSalarioBase(empleado.horaExtra);
        System.out.println("Salario: " + salario);
    }
}

Y nuestra clase estaba así:

public class empleado {
    public int salarioBase;
    public int horaExtra;
    public int tasaHoraria;

    public int calcularSalarioBase(int horaExtra) {
        return salarioBase + (tasaHoraria * horaExtra);
    }
}

A primera vista se ve bien: los datos están en empleado y el método que los usa también. Pero hay un problema serio.


El problema: cualquiera puede dañar el estado del objeto

Como los campos son public, cualquier parte del programa puede hacer esto:

empleado.salarioBase = -1;

Y en ese instante el objeto queda en un estado inválido.

Puede que tú digas: “pues no lo hagas”. Y sí, claro. Pero en un programa real, los valores no siempre los escribe un programador.

Muchas veces el salario base viene de:

  • un formulario,
  • la entrada del usuario,
  • un archivo,
  • una base de datos,
  • una API.

En cualquiera de esos casos, puede llegar un valor incorrecto.

Y si permitimos que un objeto tenga valores inválidos, el objeto deja de ser confiable. Un objeto así puede:

  • calcular mal el salario,
  • producir resultados incoherentes,
  • generar errores más adelante.

¿Y si validamos en Main con un if?

Podríamos hacer algo así:

if (salarioBase < 0) {
    System.out.println("Error: salario inválido");
}

El problema es que tendríamos que repetir esa validación:

  • en Main,
  • en otra pantalla del programa,
  • en otro módulo,
  • en cualquier lugar donde se cree o se actualice un empleado.

Eso termina siendo un caos: validaciones repetidas por todas partes, y si algún día cambias la regla (por ejemplo “no puede ser menor o igual a 0”), tendrías que actualizarla en muchos sitios.

En POO, la idea es:

Las reglas que protegen al objeto deben vivir dentro de la clase del objeto.

Ahí es donde aparecen los getters y setters.


Paso 1: ocultar el campo con private

Lo primero es impedir el acceso directo. Para eso usamos private.

Esto significa:

  • solo el código dentro de la clase puede tocar ese campo,
  • desde fuera, ya no es accesible.

Cambiamos:

public int salarioBase;

por:

private int salarioBase;

Entonces la clase queda así:

public class empleado {
    private int salarioBase;
    public int horaExtra;
    public int tasaHoraria;

    public int calcularSalarioBase(int horaExtra) {
        return salarioBase + (tasaHoraria * horaExtra);
    }
}

Y aquí pasa algo clave: si volvemos a Main e intentamos:

empleado.salarioBase = 50_000;

Java nos da un error de compilación, porque ese campo es privado.

Esto es EXACTAMENTE lo que queremos: que sea imposible modificar el salario base “a lo loco”.


Paso 2: crear un Setter (para asignar con validación)

Ahora necesitamos una forma “segura” de asignar el salario base. Creamos un método que lo haga por nosotros.

Ese método se llama setter porque sirve para set (establecer) un valor.

public void setSalarioBase(int salarioBase) {
    if (salarioBase <= 0)
        throw new IllegalArgumentException("Salario no puede ser menor o igual a 0");

    this.salarioBase = salarioBase;
}

¿Por qué usamos this.salarioBase?

Porque el parámetro se llama igual que el campo.

  • salarioBase (sin this) es el parámetro del método.
  • this.salarioBase es el campo del objeto.

Con this hacemos explícito que estamos asignando al atributo del objeto.

Nota importante: el this.salarioBase = salarioBase; debe quedar dentro del bloque correcto. La forma segura (y más clara) es usar llaves {}.

Así queda la clase correcta:

public class Empleado {
    private int salarioBase;
    public int horaExtra;
    public int tasaHoraria;

    public int calcularSalario(int horaExtra) {
        return salarioBase + (tasaHoraria * horaExtra);
    }

    public void setSalarioBase(int salarioBase) {
        if (salarioBase <= 0) {
            throw new IllegalArgumentException("Salario no puede ser menor o igual a 0");
        }
        this.salarioBase = salarioBase;
    }
}

¿Qué es throw y por qué usamos IllegalArgumentException?

Cuando una regla se rompe (por ejemplo: salario negativo), queremos detener la ejecución normal y avisar:

  • “Esto está mal”
  • “No puedo continuar así”

En Java hacemos eso con throw, que significa lanzar una excepción.

IllegalArgumentException es una excepción común para indicar:

“El argumento que me diste es inválido”.

Entonces, si alguien intenta:

empleado.setSalarioBase(-1);

el programa falla y muestra algo como:

  • IllegalArgumentException: Salario no puede ser menor o igual a 0

Esto es útil porque evita que el objeto quede en un estado inválido.


Paso 3: crear un Getter (para leer el valor)

Ok, ya podemos asignar salario base de forma segura. Pero ahora… ¿cómo lo leemos, si el campo es privado?

Antes hacíamos:

System.out.println(empleado.salarioBase);

Eso ya no se puede. Entonces creamos un método para obtener el valor. Ese método se llama getter.

public int getSalarioBase() {
    return salarioBase;
}

Con esto, desde fuera podemos leer el valor sin acceso directo al campo.


Clase final: encapsulación completa del salario base

Aquí está la versión completa con getter y setter:

public class Empleado {
    private int salarioBase;
    public int horaExtra;
    public int tasaHoraria;

    public int calcularSalario(int horaExtra) {
        return salarioBase + (tasaHoraria * horaExtra);
    }

    public void setSalarioBase(int salarioBase) {
        if (salarioBase <= 0) {
            throw new IllegalArgumentException("Salario no puede ser menor o igual a 0");
        }
        this.salarioBase = salarioBase;
    }

    public int getSalarioBase() {
        return salarioBase;
    }
}

Usarlo desde Main

Ahora Main se ve así:

public class Main {
    public static void main(String[] args) {

        var empleado = new Empleado();

        empleado.setSalarioBase(50_000);   // ✅ con validación
        empleado.tasaHoraria = 20;

        int salario = empleado.calcularSalario(10);
        System.out.println("Salario: " + salario);

        System.out.println("Salario base: " + empleado.getSalarioBase()); // ✅ lectura segura
    }
}

Fíjate en la diferencia:

  • Ya nadie puede hacer empleado.salarioBase = -1; porque es privado.
  • La única forma de asignarlo es con setSalarioBase(...), que valida.
  • La única forma de leerlo es con getSalarioBase().

¿Por qué esto es un patrón tan común en Java?

Porque así logramos una regla fundamental:

El objeto se protege a sí mismo.

En vez de depender de que “todos los demás programadores hagan lo correcto”, el objeto impone sus reglas.

Y lo mejor: como la validación está dentro de la clase, funciona en cualquier parte donde se use Empleado.


Ejercicio (muy cortico pero clave)

Haz lo mismo con tasaHoraria:

  1. conviértela en private
  2. crea setTasaHoraria(...) validando que no sea negativa
  3. crea getTasaHoraria()

Esto te deja la clase lista para el siguiente concepto: abstracción (qué mostramos hacia afuera y qué ocultamos adentro).