Control de Acceso a los Miembros de una Clase en Java

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

🔐 ¿Qué es el control de acceso en Java?

El control de acceso en Java es el mecanismo que determina qué partes de un programa pueden ver o utilizar los miembros —campos y métodos— de una clase. Mediante los modificadores de acceso (public, private, protected y el nivel por defecto package-private), el programador establece fronteras claras entre la interfaz pública de un objeto y sus detalles internos de implementación.

Los modificadores de acceso son la herramienta fundamental para aplicar el principio de encapsulamiento, uno de los cuatro pilares de la programación orientada a objetos. Un buen diseño de visibilidad protege la integridad de los datos, reduce el acoplamiento entre clases y facilita el mantenimiento del código a largo plazo. Cada miembro de una clase —ya sea un campo, un método o un constructor— debe tener asignado el nivel de acceso más restrictivo que permita cumplir su función.

💡
Concepto clave: El control de acceso no es una mera formalidad sintáctica; es una decisión de diseño que define el contrato de la clase con el resto del programa. Lo que se hace public es una promesa: el código externo dependerá de ello y cambiar su firma tendrá impacto.

Imaginemos una clase Publicacion que genera automáticamente un identificador único para cada objeto. Si el campo idPublicacion fuese accesible directamente, cualquier código externo podría modificarlo y provocar duplicados. Al declarar ese campo como private y ofrecer solo un getter público para leerlo, se protege la coherencia del sistema:

public class Publicacion {

    private static int contadorGlobal = 0;
    private int idPublicacion;
    private String titulo;

    public Publicacion(String titulo) {
        this.titulo = titulo;
        this.idPublicacion = ++contadorGlobal;
    }

    // Solo lectura: nadie externo puede cambiar el ID
    public int getId() {
        return idPublicacion;
    }

    public String getTitulo() {
        return titulo;
    }
}

En este ejemplo, el campo idPublicacion es private y no tiene setter. El único modo de obtener su valor es llamando a getId(). Así se garantiza que los identificadores se generan de forma controlada y nunca se duplican.

📋 Tabla comparativa de modificadores de acceso

Java ofrece cuatro niveles de visibilidad para campos, métodos y constructores. La siguiente tabla muestra dónde es accesible un miembro según el modificador empleado:

Modificador Misma clase Mismo paquete Subclase (otro paquete) Cualquier clase
private
(default) — sin modificador
protected
public
Regla de oro: Asigna siempre el nivel de acceso más restrictivo posible. Empieza con private y amplia solo cuando sea necesario. Así se minimizan las dependencias entre clases y se protege la integridad del objeto.

🔒 El modificador private

Un miembro declarado como private solo es accesible dentro de la propia clase donde se define. Ningún código externo —ni siquiera una subclase— puede acceder a él directamente. Es el nivel más restrictivo y el que se aplica por convención a todos los campos de instancia para lograr el encapsulamiento.

🔹 Campos privados con getters y setters

El patrón habitual consiste en declarar los campos como private y proporcionar métodos públicos para leerlos (getters) y modificarlos de forma controlada (setters):

public class Producto {

    private String nombre;
    private double precio;

    public Producto(String nombre, double precio) {
        setNombre(nombre);
        setPrecio(precio);
    }

    public String getNombre() {
        return nombre;
    }

    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 double getPrecio() {
        return precio;
    }

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

Observa cómo el constructor delega en los setters para centralizar la validación. Si mañana cambian las reglas de negocio (por ejemplo, se exige un precio mínimo de 0.01), solo hay que tocar el setter, no buscar asignaciones dispersas por todo el proyecto.

🔹 Métodos privados: lógica interna

Los métodos también pueden ser private. Se utilizan para encapsular lógica auxiliar que no debe formar parte de la interfaz pública de la clase:

public class Calculadora {

    public double calcularIVA(double base) {
        return base * obtenerTasaIVA();
    }

    // Método auxiliar: nadie externo necesita conocerlo
    private double obtenerTasaIVA() {
        return 0.21;  // 21% - podría leerse de configuración
    }
}

Si en el futuro la tasa de IVA se carga desde una base de datos en lugar de estar fija, el cambio solo afecta al método privado obtenerTasaIVA(). El método público calcularIVA() y todo el código que lo usa permanecen inalterados.

📦 Acceso package-private (default)

Cuando no se escribe ningún modificador de acceso, Java aplica el nivel package-private, también llamado acceso de paquete o default. Un miembro con este nivel es visible para todas las clases que pertenezcan al mismo paquete, pero inaccesible desde cualquier clase de otro paquete, incluidas las subclases.

package com.tienda.modelo;

class ValidadorInterno {  // Sin modificador: solo visible en com.tienda.modelo

    boolean esEmailValido(String email) {  // package-private
        return email != null && email.contains("@");
    }
}

Este nivel es útil para clases y métodos auxiliares internos que colaboran estrechamente dentro de un mismo paquete pero no deben exponerse al exterior. Es habitual en bibliotecas bien diseñadas donde las clases de implementación son package-private y solo las interfaces o clases fachada son públicas.

⚠️
Precaución: No confundir package-private con protected. El acceso por defecto no permite que las subclases de otros paquetes accedan al miembro. Si necesitas visibilidad para subclases externas, debes usar protected explícitamente.

🛡️ El modificador protected

Un miembro protected es accesible desde tres ámbitos: la propia clase, cualquier clase del mismo paquete y las subclases aunque estén en paquetes distintos. Es el nivel intermedio entre package-private y public, diseñado específicamente para facilitar la herencia controlada.

🔹 Cuándo usar protected

El uso más habitual de protected es en clases diseñadas para ser extendidas, donde ciertos campos o métodos deben ser accesibles para las subclases sin exponerlos al público general:

package com.empresa.modelo;

public class Vehiculo {

    protected String marca;
    protected int velocidadMaxima;

    public Vehiculo(String marca, int velocidadMaxima) {
        this.marca = marca;
        this.velocidadMaxima = velocidadMaxima;
    }

    protected String descripcionBase() {
        return marca + " (máx. " + velocidadMaxima + " km/h)";
    }
}
package com.empresa.transporte;  // Otro paquete

import com.empresa.modelo.Vehiculo;

public class Camion extends Vehiculo {

    private double cargaMaximaToneladas;

    public Camion(String marca, int velocidadMaxima, double cargaMaxima) {
        super(marca, velocidadMaxima);
        this.cargaMaximaToneladas = cargaMaxima;
    }

    public String descripcionCompleta() {
        // Accede a descripcionBase() porque es protected
        return descripcionBase() + " - Carga: " + cargaMaximaToneladas + "t";
    }
}

La subclase Camion, aunque está en otro paquete, puede invocar el método descripcionBase() de Vehiculo porque es protected. Si fuese package-private, la compilación fallaría; si fuese public, cualquier clase —no solo las hijas— podría acceder a él, rompiendo el diseño.

📘
Nota: Los campos protected son frecuentes en clases abstractas de frameworks como Spring o Hibernate, donde la clase base proporciona atributos comunes (ID, fecha de creación) que las entidades concretas heredan directamente.

🌐 El modificador public

Un miembro public es accesible desde cualquier clase de cualquier paquete. Es el nivel de visibilidad más amplio y se emplea para definir la interfaz pública de la clase: los métodos que el código externo puede invocar legítimamente.

🔹 Cuándo usar public

Se aplica a los métodos que conforman el contrato de la clase: getters, setters (cuando sean necesarios), métodos de lógica de negocio, y constantes que deban compartirse globalmente (public static final). También se usa para la declaración de la propia clase cuando necesita ser utilizada desde otros paquetes.

public class Conversor {

    // Constante pública: compartida globalmente
    public static final double PI = 3.141592653589793;

    // Método público: parte del contrato de la clase
    public double celsiusAFahrenheit(double celsius) {
        return celsius * 1.8 + 32;
    }

    // Método público
    public double fahrenheitACelsius(double fahrenheit) {
        return (fahrenheit - 32) / 1.8;
    }
}
⚠️
Error frecuente: Declarar campos de instancia como public «para ir más rápido». Este atajo genera deuda técnica: cuando el proyecto crece, resulta costoso y arriesgado cambiar la visibilidad porque ya hay decenas de clases accediendo directamente al campo. ❌ Nunca public para campos de instancia; ✅ siempre private con getters y setters.

🏛️ Control de acceso en clases

Los modificadores de acceso no solo se aplican a campos y métodos, sino también a las propias clases. Sin embargo, las reglas son diferentes para las clases de nivel superior (top-level) y las clases internas (inner classes).

🔹 Clases de nivel superior

Una clase de nivel superior —es decir, una clase que no está definida dentro de otra— solo puede tener dos niveles de acceso:

Modificador Visibilidad Restricción
public Visible desde cualquier paquete El nombre del archivo .java debe coincidir con el nombre de la clase
package-private (sin modificador) Visible solo dentro del mismo paquete Puede haber varias clases package-private en un mismo archivo

🔹 Clases internas y anidadas

Las clases definidas dentro de otra clase pueden usar los cuatro modificadores, incluido private. Una clase interna private solo es accesible desde la clase contenedora, lo que permite encapsular implementaciones auxiliares:

public class ListaEnlazada {

    private Nodo cabeza;

    // Clase interna privada: nadie externo la conoce
    private static class Nodo {
        Object dato;
        Nodo siguiente;

        Nodo(Object dato) {
            this.dato = dato;
        }
    }

    public void agregar(Object elemento) {
        Nodo nuevo = new Nodo(elemento);
        nuevo.siguiente = cabeza;
        cabeza = nuevo;
    }
}

La clase Nodo es un detalle de implementación de ListaEnlazada. Al declararla private, se garantiza que ningún código externo pueda crear nodos directamente ni depender de su estructura interna.

🔧 Control de acceso en constructores

Los constructores también aceptan modificadores de acceso, lo que permite controlar quién puede crear instancias de una clase. Esta técnica es fundamental en patrones de diseño como Singleton, Factory y Builder.

Modificador del constructor Quién puede instanciar Uso típico
public Cualquier clase Caso general: clases de uso libre
protected Mismo paquete + subclases Clases abstractas o base para herencia
package-private Solo clases del mismo paquete Clases internas de una biblioteca
private Solo la propia clase Singleton, Factory Method, clases de utilidad

🔹 Ejemplo: constructor privado (Singleton)

public class ConfiguracionApp {

    private static ConfiguracionApp instancia;
    private String idioma;

    // Constructor privado: nadie externo puede hacer new ConfiguracionApp()
    private ConfiguracionApp() {
        this.idioma = "es";
    }

    public static ConfiguracionApp getInstancia() {
        if (instancia == null) {
            instancia = new ConfiguracionApp();
        }
        return instancia;
    }

    public String getIdioma() { return idioma; }
    public void setIdioma(String idioma) { this.idioma = idioma; }
}

Al hacer el constructor private, se impide que se creen múltiples instancias de ConfiguracionApp. La única forma de obtener una referencia es a través del método estático getInstancia(), que controla la creación.

🏗️ Ejemplo completo integrador

El siguiente programa simula un sistema de gestión de biblioteca que combina los cuatro niveles de acceso. Cada modificador cumple un propósito de diseño específico:

package com.biblioteca.modelo;

public class Libro {

    // private: solo accesible dentro de Libro
    private String isbn;
    private String titulo;
    private boolean prestado;

    // protected: accesible por subclases (LibroDigital, LibroFisico...)
    protected String ubicacion;

    // package-private: solo visible en com.biblioteca.modelo
    int vecesPrestado;

    public Libro(String isbn, String titulo, String ubicacion) {
        setIsbn(isbn);
        setTitulo(titulo);
        this.ubicacion = ubicacion;
        this.prestado = false;
        this.vecesPrestado = 0;
    }

    // --- Métodos public: interfaz pública ---
    public String  getIsbn()    { return isbn; }
    public String  getTitulo()  { return titulo; }
    public boolean isPrestado() { return prestado; }

    public boolean prestar() {
        if (!prestado) {
            prestado = true;
            vecesPrestado++;
            return true;
        }
        return false;
    }

    public void devolver() {
        prestado = false;
    }

    // --- Métodos private: lógica interna ---
    private void setIsbn(String isbn) {
        if (isbn == null || isbn.length() != 13) {
            throw new IllegalArgumentException("ISBN debe tener 13 caracteres");
        }
        this.isbn = isbn;
    }

    private void setTitulo(String titulo) {
        if (titulo == null || titulo.isBlank()) {
            throw new IllegalArgumentException("Título requerido");
        }
        this.titulo = titulo.trim();
    }

    @Override
    public String toString() {
        return "Libro{" + titulo + ", ISBN=" + isbn +
               ", prestado=" + prestado + "}";
    }
}
package com.biblioteca.modelo;

public class LibroDigital extends Libro {

    private String formatoArchivo;  // PDF, EPUB, MOBI

    public LibroDigital(String isbn, String titulo, String formato) {
        super(isbn, titulo, "Servidor digital");
        this.formatoArchivo = formato;
        // Accede a 'ubicacion' (protected) heredado de Libro
        this.ubicacion = "Servidor digital - " + formato;
    }

    public String getFormato() { return formatoArchivo; }
}
package com.biblioteca.app;

import com.biblioteca.modelo.*;

public class SistemaBiblioteca {

    public static void main(String[] args) {

        Libro libro1 = new Libro("9788420412146", "El Quijote", "Estante A3");
        LibroDigital libro2 = new LibroDigital("9780141439518", "Pride and Prejudice", "EPUB");

        System.out.println("=== Catálogo ===");
        System.out.println(libro1);
        System.out.println(libro2);

        // Prestar libro
        libro1.prestar();
        System.out.println("\nTras prestar El Quijote:");
        System.out.println(libro1);

        // Intentar prestar un libro ya prestado
        boolean exito = libro1.prestar();
        System.out.println("Segundo préstamo exitoso: " + exito);

        // libro1.isbn = "000"; ← ERROR de compilación: isbn es private
        // libro1.vecesPrestado = 99; ← ERROR: package-private, otro paquete
    }
}

// Salida esperada:
// === Catálogo ===
// Libro{El Quijote, ISBN=9788420412146, prestado=false}
// Libro{Pride and Prejudice, ISBN=9780141439518, prestado=false}
//
// Tras prestar El Quijote:
// Libro{El Quijote, ISBN=9788420412146, prestado=true}
// Segundo préstamo exitoso: false

🐛 Errores frecuentes

⚠️
Error 1: Declarar todos los campos como public
Exponer directamente los campos permite que cualquier clase modifique el estado sin validación. Es el error más común en principiantes y genera estados inválidos difíciles de depurar.
⚠️
Error 2: Confundir package-private con protected
Un miembro sin modificador NO es accesible para subclases de otros paquetes. Si la subclase está en otro paquete y necesita acceder al miembro, hay que usar protected explícitamente. Este error genera errores de compilación confusos.
⚠️
Error 3: Hacer todo public por comodidad
Cuando todos los miembros son públicos, el compilador no detecta usos indebidos y se pierde el beneficio del encapsulamiento. A largo plazo, modificar la clase se convierte en una tarea arriesgada porque cualquier cambio puede romper código que accede directamente a los campos.
⚠️
Error 4: 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 o una vista no modificable (Collections.unmodifiableList()).
// ❌ INCORRECTO — expone la lista interna
public List<String> getTags() {
    return tags;  // El llamante puede hacer getTags().clear()
}

// ✅ CORRECTO — devuelve copia defensiva
public List<String> getTags() {
    return new ArrayList<>(tags);
}

📝 Ejercicios prácticos

Ejercicio 1: Comprensión — ¿Qué compila y qué no?

Dado el siguiente código, indica cuáles de las líneas marcadas producen error de compilación y por qué:

package paqueteA;

public class Animal {
    private String nombre;
    int edad;                      // package-private
    protected String especie;
    public String sonido;

    public Animal(String nombre, int edad, String especie, String sonido) {
        this.nombre = nombre;
        this.edad = edad;
        this.especie = especie;
        this.sonido = sonido;
    }

    public String getNombre() { return nombre; }
}
package paqueteB;
import paqueteA.Animal;

public class Perro extends Animal {

    public Perro(String nombre) {
        super(nombre, 3, "Canis lupus", "Guau");
    }

    public void mostrarDatos() {
        System.out.println(nombre);    // Línea A
        System.out.println(edad);      // Línea B
        System.out.println(especie);   // Línea C
        System.out.println(sonido);    // Línea D
    }
}

Ejercicio 2: Aplicación — Clase CuentaBancaria

Diseña una clase CuentaBancaria que cumpla estos requisitos:

  • Campos private: titular (String), saldo (double), numeroCuenta (String).
  • El numeroCuenta se genera automáticamente y no tiene setter (solo getter).
  • Métodos depositar() y retirar() con validación.
  • Método privado generarNumeroCuenta() que crea el identificador.

Ejercicio 3: Diseño — Jerarquía con protected

Crea una clase Figura con un campo protected llamado color y un método protected llamado descripcionBase(). Luego crea una subclase Circulo en otro paquete que use ambos miembros protected para construir su método toString().

❓ Preguntas frecuentes sobre Control de Acceso a los Miembros de una Clase en Java

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

Un miembro private solo es accesible dentro de la propia clase donde se declara. Un miembro protected es accesible dentro de la propia clase, desde cualquier clase del mismo paquete y desde las subclases aunque estén en paquetes diferentes. Protected amplía la visibilidad para facilitar la herencia manteniendo cierta restricción frente al acceso público.
El acceso package-private (también llamado default o de paquete) es el nivel de visibilidad que se aplica cuando no se escribe ningún modificador de acceso. Los miembros con este nivel son visibles para todas las clases del mismo paquete, pero inaccesibles desde clases de otros paquetes, incluidas las subclases que estén fuera del paquete.
Declarar los atributos como private es una buena práctica del principio de encapsulamiento. Al ocultar los datos internos se impide que código externo los modifique de forma incontrolada, se puede validar los valores antes de asignarlos mediante setters, y se facilita cambiar la implementación interna sin afectar al código cliente que depende de la clase.
No. Una clase de nivel superior (top-level) solo puede ser public o package-private (sin modificador). Sin embargo, las clases internas (inner classes) y las clases anidadas estáticas sí pueden declararse como private, lo que las hace accesibles únicamente desde la clase contenedora.
Se recomienda usar protected cuando se diseña una clase pensada para ser extendida por herencia y se necesita que las subclases accedan directamente a ciertos miembros sin necesidad de getters públicos. Es habitual en frameworks y bibliotecas donde se proporcionan métodos gancho (hook methods) que las subclases deben poder sobreescribir o utilizar.
Valora este artículo

💬 Foro de discusión

¿Tienes dudas sobre Control de Acceso a los Miembros de una Clase en Java? Comparte tu pregunta con la comunidad.

¿Tienes cuenta? o comenta como invitado ↓

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