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