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. There’s one simplification. It does not also leak to the same package, because 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 follow from that. Java code can technically call an internal Kotlin member, since 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("a@b.com") // 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. First, public is the default, so you annotate to hide rather than to reveal. Second, internal gives a module-wide scope Java never had — the right tool for a library’s shared-but-not-exported guts. The caveat: 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