Modificadores de acceso Kotlin: public, protected, private
Los modificadores de acceso en Kotlin (public, internal, protected y private) deciden quién puede ver cada clase, función o propiedad.
Esconder lo que no hace falta enseñar tiene un nombre: encapsulación. Cuanto menos expone una clase, menos cosas se rompen cuando la cambias.
Los modificadores de acceso, en una tabla
| Modificador | Dentro de una clase, lo ve… | Fuera de una clase (nivel superior), lo ve… |
|---|---|---|
public (por defecto) |
Cualquiera que vea la clase | Todo el mundo |
internal |
Cualquiera del mismo módulo que vea la clase | El mismo módulo |
protected |
La clase y sus subclases | No se permite |
private |
Solo esa clase (con todos sus miembros) | Solo ese fichero |
Es un resumen nuestro de la página Visibility modifiers de la documentación oficial. Un módulo es un conjunto de ficheros Kotlin que se compilan juntos: un módulo de IntelliJ IDEA, un proyecto de Maven o un source set de Gradle.
Los cuatro modificadores dentro de una clase
Para verlos de verdad hacen falta dos módulos. En el primero, tienda, está esta clase:
package tienda
class Pedido(private val codigoTarjeta: String) { // private val en el constructor
val total = 42 // public por defecto
internal val margen = 7 // solo dentro del módulo «tienda»
private val coste = 35 // solo dentro de Pedido
var estado = "nuevo" // se lee desde fuera...
private set // ...pero solo Pedido lo cambia
fun enviar() {
estado = "enviado"
println("Tarjeta ****${codigoTarjeta.takeLast(4)}, coste $coste, margen $margen")
}
}
internal fun calcularDescuento(pedido: Pedido) = pedido.margen / 2 // dentro del módulo, sí
Desde otro módulo, app, que usa tienda como dependencia, solo se puede tocar lo público:
import tienda.Pedido
fun main() {
val pedido = Pedido("4111111111111111")
pedido.enviar()
println("${pedido.total} ${pedido.estado}")
}
Tarjeta ****1111, coste 35, margen 7
42 enviado
Si app intenta llegar a lo demás, el compilador lo para:
import tienda.Pedido
import tienda.calcularDescuento
fun main() {
val pedido = Pedido("4111111111111111")
println(pedido.margen)
println(calcularDescuento(pedido))
pedido.estado = "cancelado"
println(pedido.coste)
}
app/ErrInternal.kt:2:15: error: cannot access 'fun calcularDescuento(pedido: Pedido): Int': it is internal in file.
app/ErrInternal.kt:6:20: error: cannot access 'val margen: Int': it is internal in 'tienda.Pedido'.
app/ErrInternal.kt:7:13: error: cannot access 'fun calcularDescuento(pedido: Pedido): Int': it is internal in file.
app/ErrInternal.kt:8:12: error: cannot access 'estado': it is private in 'tienda.Pedido'.
app/ErrInternal.kt:9:20: error: cannot access 'val coste: Int': it is private in 'tienda.Pedido'.
Para compilarlo así, cada módulo se compiló por separado con kotlinc -module-name: tienda a un .jar y app contra ese .jar. En un proyecto real lo hace Gradle o Maven.
private val y private set
Dos usos de private que aparecen en casi cualquier clase de Kotlin:
private valen el constructor:class Pedido(private val codigoTarjeta: String)crea una propiedad que solo vePedido. Convala secas, el número de la tarjeta sería público.private set: la propiedad se lee desde fuera, pero solo la propia clase puede cambiarla.estadoespublicpara leer yprivatepara escribir. El getter, en cambio, siempre tiene la misma visibilidad que la propiedad.
protected en Kotlin
protected significa lo mismo que private, más las subclases. Solo tiene sentido en clases que se pueden heredar: las marcadas con open o abstract.
open class Base {
protected open fun calcular() = 1
}
class Hija : Base() {
override fun calcular() = 2 // hereda la visibilidad: sigue siendo protected
fun usar() = calcular()
}
class Publica : Base() {
public override fun calcular() = 3 // se puede ampliar a public
}
fun main() {
println(Hija().usar())
println(Publica().calcular())
}
2
3
Tres reglas que se ven en el ejemplo:
-
Al redefinir, se conserva la visibilidad. Si no escribes ninguna, el
overridede un miembroprotectedsigue siendoprotected. Desde fuera,Hija().calcular()no compila:error: cannot access 'fun calcular(): Int': it is protected in 'Hija'. -
Se puede abrir más, como hace
Publicaconpublic override. -
No se puede cerrar más. Si el padre lo tiene
public, la hija no puede redefinirlo comoprotected:open class Base { open fun mostrar() = "base" } class Cerrada : Base() { protected override fun mostrar() = "hija" }error: cannot weaken access privilege protected for 'mostrar' in 'Base'.
Y lo que más sorprende si vienes de Java: otra clase del mismo paquete no ve lo protected:
package tienda
open class Producto {
protected val precioCoste = 10
}
class Inventario { // mismo paquete, pero no es subclase
fun valor(p: Producto) = p.precioCoste
}
err/MismoPaquete.kt:8:32: error: cannot access 'val precioCoste: Int': it is protected in 'tienda.Producto'.
fun valor(p: Producto) = p.precioCoste
^^^^^^^^^^^
protected tampoco se admite fuera de una clase ni dentro de una interfaz.
private: privado de la clase, no del objeto
En Kotlin, private dentro de una clase significa «privado de la clase», no «privado de este objeto». El código de una clase ve lo privado de cualquier instancia de esa misma clase:
class Cuenta(private val saldo: Int) {
fun tieneMasQue(otra: Cuenta) = saldo > otra.saldo // otra instancia distinta
}
fun main() {
println(Cuenta(100).tieneMasQue(Cuenta(50)))
}
true
otra es otro objeto y, aun así, Cuenta lee su saldo privado.
Lo que no ve una clase es lo privado de sus clases anidadas. Aquí Kotlin también es más estricto que Java:
class Caja {
class Cajon {
private val secreto = "llave"
}
fun abrir(c: Cajon) = c.secreto
}
err/Anidada.kt:5:29: error: cannot access 'val secreto: String': it is private in 'Caja.Cajon'.
fun abrir(c: Cajon) = c.secreto
^^^^^^^
Los constructores también admiten modificadores: con class Termometro private constructor(val celsius: Double), nadie de fuera puede escribir Termometro(20.0). Se usa para obligar a crear los objetos con funciones de fábrica, como se explica en Companion object en Kotlin.
internal: visible en el mismo módulo
internal es lo que más cuesta al principio, porque Java no tiene nada igual. Lo ve todo el código del mismo módulo, aunque esté en otro paquete, y nadie de fuera. Sirve para lo que una librería o un módulo necesita compartir entre sus ficheros, pero no quiere ofrecer como parte de su API.
Dos detalles de la documentación:
-
Los tests ven lo
internal. En Gradle cada source set cuenta como un módulo, con una excepción: el source settestpuede acceder a las declaracionesinternaldemain. -
Desde Java,
internalse convierte enpublic, pero el compilador cambia el nombre de los miembros en el bytecode. Según la documentación, así se evitan redefiniciones accidentales entre módulos. Se ve al inspeccionar la clasePedidoconjavap:public final int getTotal(); public final int getMargen$tienda(); public final java.lang.String getEstado();getMargen$tiendalleva añadido el nombre del módulo. Java puede llamarlo, así queinternalno es una barrera para el código Java.
Modificadores de acceso fuera de las clases
En Kotlin puedes escribir funciones, propiedades y clases directamente en un fichero, sin clase que las contenga (declaraciones de nivel superior). También admiten modificadores, con dos matices: private significa «solo este fichero» y protected no está permitido.
// Utilidades.kt
package utilidades
private fun limpiar(texto: String) = texto.trim().lowercase() // solo este fichero
fun normalizar(texto: String) = limpiar(texto) // public: todos
// Main.kt, en el mismo paquete
package utilidades
fun main() {
println(normalizar(" HOLA "))
println(limpiar(" HOLA "))
}
top/Main.kt:6:13: error: cannot access 'fun limpiar(texto: String): String': it is private in file.
println(limpiar(" HOLA "))
^^^^^^^
Aunque los dos ficheros estén en el mismo paquete, limpiar() solo existe para Utilidades.kt. Sin esa línea, el programa imprime hola.
Con protected fuera de una clase, o con cualquier modificador en una variable local, el compilador protesta:
err/ProtectedTop.kt:1:1: error: modifier 'protected' is not applicable to 'top level function'.
err/Local.kt:2:5: error: modifier 'private' is not applicable to 'local variable'.
Diferencias con Java: package-private e internal
| Java | Kotlin | |
|---|---|---|
| Sin modificador | Visible en el paquete (package-private) | public |
| Visibilidad de paquete | Sí, sin modificador | No existe |
| Visibilidad de módulo | No hay modificador equivalente | internal |
protected |
La clase, sus subclases y todo el paquete | La clase y sus subclases |
private |
Todo el cuerpo de la clase de nivel superior, también las anidadas | Solo esa clase; la exterior no ve lo privado de las anidadas |
private fuera de clases |
No aplica: todo método o campo pertenece a una clase o interfaz | Visible solo en el fichero |
La tabla es nuestra, a partir de la documentación de Kotlin y de la especificación del lenguaje Java SE 25 (§6.6.1). Java tiene, además, la visibilidad entre módulos del sistema de módulos (exports), que es otro mecanismo y aquí no se compara.
El mismo caso en Java, compilado con javac 25.0.5, sí funciona:
package tienda;
public class Producto {
protected int precioCoste = 10; // en Java, protected incluye el paquete
int stock = 3; // sin modificador: package-private
}
package tienda;
public class Inventario { // mismo paquete, no es subclase
public static void main(String[] args) {
Producto p = new Producto();
System.out.println(p.precioCoste + " " + p.stock);
System.out.println(new Caja().abrir(new Caja.Cajon()));
}
}
class Caja {
static class Cajon {
private String secreto = "llave";
}
String abrir(Cajon c) { return c.secreto; } // Java: la clase exterior ve lo private de la anidada
}
10 3
llave
Si traduces código de Java a Kotlin, revisa sobre todo dos casos: lo que no tenía modificador (en Kotlin pasará a ser public, salvo que lo marques internal o private) y lo protected que usaban otras clases del paquete. Hay más diferencias entre los dos lenguajes en Kotlin vs Java.
open: permiso para heredar
protected solo se nota cuando hay herencia, y en Kotlin las clases son final por defecto: no se puede heredar de ellas si no las marcas con open. En Java es al revés: se puede heredar salvo que la clase sea final.
class Final
class HijaDeFinal : Final()
Open.kt:2:21: error: this type is final, so it cannot be extended.
class HijaDeFinal : Final()
^^^^^
Con las funciones pasa lo mismo: para redefinir una, tiene que ser open en el padre y llevar override en la hija. Si la hija declara otra con el mismo nombre sin override, no compila:
open class Padre {
open fun saludar() = "hola"
}
class Hija : Padre() {
fun saludar() = "adiós"
}
Hides.kt:5:9: error: 'saludar' hides member of supertype 'Padre' and needs an 'override' modifier.
fun saludar() = "adiós"
^^^^^^^
El ejemplo original, paso a paso
La versión de 2018 de este artículo usaba este ejemplo, que se mantiene porque junta los cuatro modificadores con herencia:
open class AccessModifiers {
val prop1 = "Public by default"
private val prop2 = "Marked as private"
protected val prop3 = "Marked as protected"
internal val prop4 = "Marked as internal"
protected fun showCase(): String {
val test = AccessModifiers()
// Dentro de la clase se ven las tres, también en otra instancia
return listOf(test.prop1, test.prop2, test.prop3, this.prop1, prop1).joinToString()
}
}
class AccessModifiedChild : AccessModifiers() {
fun showCaseX(): String {
// prop2 // No compila: es private de AccessModifiers
return listOf(showCase(), prop1, prop3, prop4).joinToString(" | ")
}
}
class AnotherClass {
fun showCase(): String {
val test = AccessModifiers()
// test.prop2 y test.prop3 no compilan aquí
return test.prop1 + " / " + test.prop4
}
}
fun main() {
println(AccessModifiedChild().showCaseX())
println(AnotherClass().showCase())
}
Public by default, Marked as private, Marked as protected, Public by default, Public by default | Public by default | Marked as protected | Marked as internal
Public by default / Marked as internal
-
AccessModifiersesopenpara que se pueda heredar de ella.showCase()esprotected: la usan la clase y sus subclases, nadie más.this.prop1yprop1son lo mismo, la propiedad del objeto actual;test.prop1es la del otro objeto. -
AccessModifiedChildhereda y veprop1(pública),prop3(protegida) yprop4(interna).prop2no:error: cannot access 'val prop2: String': it is private in 'AccessModifiers'.Su función se llama
showCaseX()porque la del padre no esopeny, con el mismo nombre, daría el error dehides memberde la sección anterior. -
AnotherClassno hereda: solo veprop1, pública, yprop4, que esinternaly está en el mismo módulo.prop3no:error: cannot access 'val prop3: String': it is protected in 'AccessModifiers'.
Empieza por private y abre solo lo que haga falta.
Errores típicos
- Pensar que sin modificador es «de paquete», como en Java. En Kotlin es
public. - Usar
protectedpara compartir con el paquete. No funciona: o es una subclase, o necesitasinternal. - Poner
protecteden una clasefinal. Compila, pero no aporta nada, porque nadie puede heredar de ella. Márcalaopeno usaprivate. - Dejar público un
varque solo debería cambiar la clase.private setlo resuelve sin escribir un getter a mano. - Creer que
privateprotege de otras instancias. Protege de otras clases; dentro de la clase, cualquier instancia ve lo privado de las demás.
Cuál elegir
Esto es criterio nuestro, no de la documentación: empieza por la visibilidad más cerrada que funcione y ábrela cuando haga falta.
privatepara todo lo que es detalle de la implementación.protectedsolo si diseñas una clase para que se herede y las subclases lo necesitan.internalpara lo que comparte un módulo entre sus ficheros sin ofrecerlo fuera.publicpara la API: lo que quieres que otros usen y estás dispuesto a mantener.
Cerrar algo público más adelante puede romper el código de quien ya lo usa; abrir algo privado, no.
Siguiente paso
- Interfaces en Kotlin: por qué dentro de una interfaz no se admiten
protectedniinternal. - Companion object en Kotlin: el patrón del constructor
privatecon funciones de fábrica. - Objetos y estructuras de datos: por qué conviene esconder los detalles de una clase.
- ¿Qué es Kotlin?, si estás empezando con el lenguaje.
Fuentes
- Documentación oficial de Kotlin, consultada el 9 de noviembre de 2026: Visibility modifiers, Inheritance y Calling Kotlin from Java: visibility.
- The Java Language Specification, Java SE 25 Edition, §6.6.1 Determining Accessibility, consultada el 9 de noviembre de 2026.
- Todo el código y los errores de compilación se han ejecutado con
kotlinc-jvm2.4.21 (zip oficial de las releases de GitHub) yjavac25.0.5 sobre OpenJDK 25.0.5, en la imagen Dockereclipse-temurin:25. Los dos módulos se compilaron por separado con-module-name. En los mensajes de error más largos solo se muestran las líneas que importan.