Nivel inicial14 min de lectura · Publicado el

Modificadores de acceso Kotlin: public, protected, private

Modificadores de acceso en Kotlin: private, solo la clase; protected, también sus subclases; internal, el mismo módulo; y public, todos. Visibilidad: ¿quién lo ve?rubentejera.com privatesolo la claseprotectedla clase y sus subclasesinternalel mismo módulopublictodos (por defecto)
Quién puede ver cada parte de una clase. public es el valor por defecto; internal abre a todo el módulo (los ficheros que se compilan juntos), y protected, a diferencia de Java, no incluye el paquete.

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 val en el constructor: class Pedido(private val codigoTarjeta: String) crea una propiedad que solo ve Pedido. Con val a 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. estado es public para leer y private para 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 override de un miembro protected sigue siendo protected. Desde fuera, Hija().calcular() no compila:

    error: cannot access 'fun calcular(): Int': it is protected in 'Hija'.
  • Se puede abrir más, como hace Publica con public override.

  • No se puede cerrar más. Si el padre lo tiene public, la hija no puede redefinirlo como protected:

    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 set test puede acceder a las declaraciones internal de main.

  • Desde Java, internal se convierte en public, 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 clase Pedido con javap:

    public final int getTotal();
    public final int getMargen$tienda();
    public final java.lang.String getEstado();

    getMargen$tienda lleva añadido el nombre del módulo. Java puede llamarlo, así que internal no 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
  • AccessModifiers es open para que se pueda heredar de ella. showCase() es protected: la usan la clase y sus subclases, nadie más. this.prop1 y prop1 son lo mismo, la propiedad del objeto actual; test.prop1 es la del otro objeto.

  • AccessModifiedChild hereda y ve prop1 (pública), prop3 (protegida) y prop4 (interna). prop2 no:

    error: cannot access 'val prop2: String': it is private in 'AccessModifiers'.

    Su función se llama showCaseX() porque la del padre no es open y, con el mismo nombre, daría el error de hides member de la sección anterior.

  • AnotherClass no hereda: solo ve prop1, pública, y prop4, que es internal y está en el mismo módulo. prop3 no:

    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 protected para compartir con el paquete. No funciona: o es una subclase, o necesitas internal.
  • Poner protected en una clase final. Compila, pero no aporta nada, porque nadie puede heredar de ella. Márcala open o usa private.
  • Dejar público un var que solo debería cambiar la clase. private set lo resuelve sin escribir un getter a mano.
  • Creer que private protege 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.

  1. private para todo lo que es detalle de la implementación.
  2. protected solo si diseñas una clase para que se herede y las subclases lo necesitan.
  3. internal para lo que comparte un módulo entre sus ficheros sin ofrecerlo fuera.
  4. public para 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

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-jvm 2.4.21 (zip oficial de las releases de GitHub) y javac 25.0.5 sobre OpenJDK 25.0.5, en la imagen Docker eclipse-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.