Kotlin Has a Visibility Level Java Doesn't: internal

The four visibility modifiers, why public is the default, what internal means at the module level and how it gets name-mangled in bytecode, per-accessor visibility, and how Kotlin's file structure frees you from one-class-per-file.

Java has four visibility levels but only three keywords — the fourth, package-private, is what you get by saying nothing. Kotlin also has four, with a different lineup: public, private, protected, and a new one, internal. The defaults differ, and so does the file structure they live in. None of it is hard, but a Java developer’s instincts are slightly off, so it’s worth a reset. Everything here was run against Kotlin 2.4.10.

This chapter follows destructuring. It’s about who gets to see what.

public is the default

In Java a declaration with no modifier is package-private. In Kotlin a declaration with no modifier is public — visible everywhere. You almost never type public:

class Service          // public
fun helper() { }       // public

The flip is deliberate. Most things you write are meant to be used, so the common case is the quiet one: you add a modifier to restrict, not to expose. Notably, Kotlin has no package-private level — there’s no “visible within the package” tier, because packages in Kotlin don’t carry the access meaning they do in Java.

private and protected

private means visible inside its enclosing scope — for a class member, the class; for a top-level declaration, the file:

class Account { private var balance = 0 }   // visible only inside Account
private fun audit() { }                       // visible only in this file

protected works as in Java for class members — the class and its subclasses — with one simplification: it does not also leak to the same package (there’s no package-private level for it to blend into). A protected member in Kotlin is strictly for subclasses.

internal: visible within the module

The one with no Java equivalent. internal means visible everywhere within the same module and invisible outside it. A module is a set of files compiled together — a Gradle source set, a Maven module, one IntelliJ module:

internal class Engine    // usable across the module, hidden from consumers

It fills a real gap. When you publish a library, public is your API and private is per-file; neither lets you share a class freely across your own code while keeping it off the public surface. internal is exactly that — “public to us, private to them.” If you write libraries you’ll use it constantly; in an app module, less so.

One boundary detail that surprises people the first time reflection or Java-interop is involved: internal doesn’t exist in JVM bytecode, which has no module concept. So the compiler emits internal members as public but mangles their names — a method doWork becomes something like doWork$mymodule. The practical consequences: Java code can technically call an internal Kotlin member (the mangled name is public), and a @JvmName is sometimes needed if you want a clean name across the boundary; and reflection sees the mangled name, not doWork. Within Kotlin the module boundary is enforced cleanly; it’s only at the JVM edge that the fiction shows.

Files don’t dictate structure

Java ties you to one public class per file, named after the file. Kotlin drops that rule. A single .kt file can hold any number of top-level classes, functions, and properties:

// Geometry.kt
class Point(val x: Int, val y: Int)
class Line(val from: Point, val to: Point)
fun distance(a: Point, b: Point): Double = /* ... */ 0.0

The package line at the top also need not mirror the directory layout the way Java enforces — convention suggests matching them for sanity, but the compiler doesn’t require it. The upshot: group small, related declarations by topic into one file rather than scattering them across a directory of single-class files. A Geometry.kt holding a few related shapes and helpers is idiomatic, not a smell.

Different visibility for a getter and setter

A common need: readable everywhere, writable only inside. The setter carries its own, stricter modifier:

class Counter {
    var value: Int = 0
        private set            // public to read, private to write
    fun increment() { value++ }
}

No separate backing field or hand-written getter required. A getter, by contrast, can’t be more visible than its property — the property’s visibility is the ceiling.

Restricting constructors

Marking a constructor private funnels creation through a factory, enforcing validation or handing back a cached instance:

class Email private constructor(val address: String) {
    companion object {
        fun of(raw: String): Email? = if ("@" in raw) Email(raw) else null
    }
}
Email.of("[email protected]")     // Email?, validated
// Email("nonsense")    // won't compile — constructor is private

A worked example

One class can use every level at once, each chosen by who needs the member:

open class BankAccount internal constructor(   // created only within the module
    val id: String,                            // public: part of the contract
) {
    var balance: Long = 0
        private set                            // read anywhere, write only here
    internal fun audit(): Long = balance       // module tooling can peek
    protected open fun onOverdraft() { }       // subclasses can react
    private fun applyFee() { balance -= 25 }   // pure internal mechanism
}

Reading the modifiers tells you the intended audience of each member at a glance, which is the whole point of having four.

Final thoughts

Two shifts to absorb: public is the default, so you annotate to hide rather than to reveal; and internal gives a module-wide scope Java never had, the right tool for a library’s shared-but-not-exported guts — with the caveat that it evaporates into a mangled public name at the JVM boundary. Pair that with files that hold whatever declarations belong together, and you organize code by what it’s about, not by a one-type-per-file rule.

Next, the by keyword and the pattern it unlocks: handing off work to another object without the boilerplate. That’s delegation.

Practice: reinforce this with the companion workbook — short, click-to-reveal problems.

Comments