Kotlin Has a Visibility Level Java Doesn't: internal


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 too, 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 quick reset.

This post 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 the word public; it’s what you get for free:

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.

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 — visible to the class and its subclasses — with one simplification: it does not also leak to the same package the way Java’s protected does. There’s no package-private level for it to blend into.

internal: visible within the module

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

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 out of the public surface. internal is exactly that — your module’s “public to us, private to them.” If you write libraries, you’ll use it constantly; in an app module, less so.

Files don’t dictate structure

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

// 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 = /* ... */

And the package is declared with a package line at the top — it does not have to mirror the directory layout the way Java enforces. Convention still suggests matching them for sanity, but the compiler doesn’t require it.

The practical upshot: group small, related declarations by topic into one file instead of scattering them across a directory of single-class files. A file named Geometry.kt holding a few related shapes and helpers is idiomatic, not a code smell.

Different visibility for a getter and setter

A common need: a property the world can read but only the class can change. Kotlin lets the setter carry its own, stricter modifier:

class Counter {
    var value: Int = 0
        private set            // public to read, private to write

    fun increment() { value++ }
}

Callers see counter.value but can’t assign to it — no separate backing field or hand-written getter required.

Restricting constructors

Marking a constructor private funnels creation through a factory, which is how you enforce validation or hand 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

protected in practice

protected opens a member to subclasses but no one else — useful for a hook that extenders override or call, while staying invisible to ordinary callers:

abstract class Repository {
    protected abstract fun connect(): Connection   // for subclasses only

    fun query(sql: String): Result {
        val c = connect()
        return c.run(sql)
    }
}

A worked example

One class can use every level at once, each chosen by who needs to see 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 entire point of having four of them.

Final thoughts

Two shifts to absorb: public is the default, so you annotate to hide rather than to reveal; and internal gives you a module-wide scope that Java never had, which is the right tool for a library’s shared-but-not-exported guts. Pair that with files that can hold whatever set of 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.