🔐 ¿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.
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 |
✅ | ✅ | ✅ | ✅ |
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.
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.
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; } }
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
publicExponer 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.
protectedUn 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.public por comodidadCuando 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.
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
numeroCuentase genera automáticamente y no tiene setter (solo getter). - Métodos
depositar()yretirar()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.
💬 Foro de discusión
¿Tienes dudas sobre Control de Acceso a los Miembros de una Clase en Java? Comparte tu pregunta con la comunidad.
Todavía no hay mensajes. ¡Sé el primero en participar!