JavaRush/Courses/Kotlin SELF/Encapsulation and property visibility:

Encapsulation and property visibility: public / private / internal

Open

1. Encapsulation and object state

When you write a class, it’s as if you’re creating a small universe: inside there is state (properties) and rules of life (methods). And if you throw the doors of this universe wide open, any external code can walk in and throw a fireworks party… in the server room. Encapsulation is not “hide everything from everyone”, but “make it so that state can be changed only in safe ways”.

In Kotlin this is solved very practically: we choose which properties should be available to everyone (public), which only to the class itself (private), and which only within the project/module (internal). And the earlier you start thinking about this as a habit, the fewer situations you’ll have like “why did my balance become -1000, I didn’t do anything like that”.

What object state is and how easy it is to break

State is the values of properties inside an object. The problem is that even the friendliest developer (especially future you two weeks from now) can accidentally assign something ridiculous there. Bugs from this are usually unpleasant: the program compiles, starts, but the logic begins to “drift”.

Let’s take a simple example — our mini project “expense tracking”. Suppose we have an expense:

class Expense(
    val title: String,
    val amount: Int
)

Now let’s create an “expense book” the naive way — just a public list:

class ExpenseBook {
    val expenses: MutableList<Expense> = mutableListOf()
}

It looks convenient: add, delete, sort — do whatever you want. But that’s exactly the problem: external code gets direct access to the object’s internals.

fun main() {
    val book = ExpenseBook()

    book.expenses.add(Expense("Coffee", 250))
    book.expenses.clear() // oops: deleted all expenses
}

At this point your ExpenseBook stops being a “book with rules” and becomes just a data bag with no guarantees. If you wanted, for example, to forbid deleting all expenses in one go or to keep change statistics — you no longer control anything. Encapsulation starts where a class says: “From the outside — only what I allow”.

2. Visibility modifiers in Kotlin

Basic modifiers: public, private, internal

To control access to properties, Kotlin provides visibility modifiers. They apply to properties, functions, classes, and even top-level declarations (that is, functions/constants outside classes). The most important ones for today’s lecture are these three:

Modifier Visible where How to think about it in plain English
public
everywhere “this is part of the class’s public API”
private
only inside the class (or inside the file for top-level) “this is the internal kitchen”
internal
within a single module “OK for the project, not OK outside”

Key detail: if you don’t write anything, the default is public. That is, “open to everyone”. It’s convenient to start with, but dangerous as a permanent habit.

A separate nuance: private for top-level declarations works at the file level — meaning private top-level functions/variables are visible within a single .kt file. This is used directly in the documentation as an example of visibility for extensions and top-level details.

public by default: when it’s fine, and when it’s already suspicious

public is not evil. It’s just an “open door”. If you truly want a property to be part of the class contract, make it public. Most often this is fine for data that should not change, i.e. for val.

For example, an expense can be immutable: the title and amount are set at creation time and then don’t bounce back and forth like a cat’s mood.

class Expense(
    val title: String,
    val amount: Int
)

But even here there’s a risk: amount can be negative. In previous lectures we discussed validation via require(), so let’s improve the model and protect the invariant “amount > 0”:

class Expense(
    val title: String,
    val amount: Int
) {
    init {
        require(title.isNotBlank()) { "title must not be blank" }
        require(amount > 0) { "amount must be > 0" }
    }
}

Now Expense at least can’t be “broken” from the moment it’s created.

But a public var is already a “be careful” signal. If you make a mutable property public, you allow external code to change the object’s state directly, bypassing any rules. Sometimes this is justified (for example, in very simple educational examples), but in real code it’s almost always a future source of weirdness.

private: the main encapsulation tool

private is a way to say: “No hands in here from the outside.” In design terms it almost always means: storage is private, changes are through methods.

Let’s return to ExpenseBook. We want the expense list to live inside the book, but not stick out like an uninsulated wire. Let’s make the list private and provide safe operations.

class ExpenseBook {
    private val expenses: MutableList<Expense> = mutableListOf()

    fun add(expense: Expense) {
        expenses.add(expense)
    }

    fun all(): List<Expense> = expenses.toList() // snapshot, not the original
}

Notice an important trick: we return List<Expense>, not MutableList<Expense>, and we call toList(). This means external code gets a copy (a snapshot), not a reference to the internal list. Yes, copying costs some resources, but for a learning project it’s a great way to feel the idea “don’t expose internals as-is”.

Let’s verify that it’s now harder to “break” the object:

fun main() {
    val book = ExpenseBook()
    book.add(Expense("Coffee", 250))

    val snapshot = book.all()
    println(snapshot.size) // 1

    // snapshot.add(...) // can't: List has no add()
    // book.expenses.clear() // can't: expenses is private
}

From a beginner’s perspective it can feel like “why is Kotlin getting in my way?!”. But it’s not Kotlin — it’s you putting the right fence around state so you won’t chase bugs at night later.

Another very common technique is to make a constructor parameter a private property. This is useful when the value is needed inside the class, but you don’t want to expose it outside:

class User(private val userId: String) {
    fun label(): String = "User($userId)"
}

From the outside userId is not visible, but the class can use it for its methods. This design really helps avoid turning every class into a “public stand with all the wires sticking out”.

internal: visible to the module, but not to “the outside world”

internal is a compromise between public and private. It says: “This can be used within our module, but from the outside (for example, if we build a library) it shouldn’t exist.”

It’s important to understand what a module is. In a simple Kotlin/JVM project, a module usually matches a Gradle module (often the entire project if there’s only one). So internal is convenient for details that are needed by different files/packages within the project, but should not become part of the public API.

A simple example:

internal class BuildInfo(
    val buildType: String
)

fun main() {
    val info = BuildInfo("debug")
    println(info.buildType) // debug
}

If you later turn the project into a library, code from another module won’t be able to use BuildInfo. Inside — go ahead.

And one more important point: internal applies not only to properties and classes, but also to functions, including extension functions. The Kotlin documentation explicitly emphasizes this: an internal extension is available only within the module.

This is a good mental guideline: internal = our project’s internal infrastructure.

3. Practical design and the ExpenseBook example

What to make public, what to hide

When you design a class, it helps to mentally split it into “contract” and “implementation”. The contract is how the class allows itself to be used. The implementation is how it’s built internally. Encapsulation is the boundary between these two layers.

Let’s lock in a few practical rules (no fanaticism, but as good “default brain settings”). If a property reflects a fact and should not change after the object is created, usually public val is enough. If a property is tied to internal state and can change, it’s almost always better to make it private and change it via methods, so you have one place where rules are checked. If a property or class is needed by several parts of the project but you don’t want to promise it to external users, choose internal.

In the next lecture we’ll add even finer control via getters/setters and private set, but today the goal is to learn to properly close off “the insides” so an object doesn’t turn into an uncontrolled bag of data.

A small reference table:

Situation Best approach
“This value is part of the model; it’s set at creation time and doesn’t change”
public val
“This is internal storage; it must not be changed directly”
private var/val + public methods
“Needed by different project files, but we don’t want to expose it outside”
internal
“We want to provide access to a collection, but without the right to mutate it”
private MutableList, outside: List (copy or read-only view)

Growing the mini app: ExpenseBook as the state owner

Now we’ll assemble a mini version that already looks like a proper domain model. We’ll have Expense (an immutable expense) and ExpenseBook (an expense book) that owns the collection and provides clear operations.

First, Expense:

class Expense(
    val title: String,
    val amount: Int
) {
    init {
        require(title.isNotBlank()) { "title must not be blank" }
        require(amount > 0) { "amount must be > 0" }
    }
}

Now ExpenseBook — note the private list and public methods:

class ExpenseBook {
    private val expenses: MutableList<Expense> = mutableListOf()

    fun add(title: String, amount: Int) {
        expenses.add(Expense(title, amount))
    }

    fun totalAmount(): Int = expenses.sumOf { it.amount }
}

And an example of usage in main (very short, just to see the idea):

fun main() {
    val book = ExpenseBook()

    book.add("Coffee", 250)
    book.add("Transit", 70)

    println("Total = ${book.totalAmount()}") // Total = 320
}

What matters here is that the state (expenses) lives only inside ExpenseBook. From the outside you work through meaningful operations add(...) and totalAmount(). That’s exactly “the class is responsible for its invariants”: external code can’t suddenly clear the list, slip in an Expense with a negative amount (because Expense won’t be created), or replace the collection entirely.

4. Typical mistakes when choosing visibility

Mistake #1: making everything public “because it’s easier”.
It is easier… until the first bug. When a class has a bunch of public var, you lose control over state: you don’t know who changed the property, where, and when. As a result the class stops being a “type with behavior” and turns into a set of fields anyone can mess up. It’s better to start with a more closed design and open only what you truly need.

Mistake #2: exposing a mutable collection as-is.
public val expenses: MutableList<Expense> looks harmless, but it’s effectively “remote control of your object”. From the outside someone can call clear(), removeAt(), sort(), and your class won’t even know what happened. It’s safer to keep a MutableList private and expose a List (either a copy or a read-only view). Otherwise you break encapsulation yourself, even if you put nice require(...) calls in the constructor.

Mistake #3: confusing private with “impossible to use at all”.
Beginners sometimes perceive private as punishment: “I made it private — and now nothing works.” In reality the idea is that you must provide another interaction mechanism: methods that express intent (add, totalAmount, removeLast, etc.). Encapsulation is not “hide it and forget it”, it’s “control access”.

Mistake #4: using internal without understanding the module boundary.
If you think internal is “almost private”, you can accidentally build an architecture where half the project depends on the internal details of the other half, and then it will hurt to extract code into a library or split into modules. Keep a simple rule: internal is details for “our project”, not for future users and not for neighboring modules.

Mistake #5: trying to “manually watch” invariants from external code.
Sometimes you see a style like: “Let properties be public and I’ll be careful.” This works until the code starts growing and you get another “me” who never saw your promises. Invariants should live next to state — inside the class — not in the developer’s head (the head already has a queue of tasks, coffee, and existential questions anyway).

1
Task
Kotlin SELF,  32 level,  0 lecture
Locked
Secret Counter
Secret Counter
1
Task
Kotlin SELF,  32 level,  0 lecture
Locked
Shopping list
Shopping list
1
Task
Kotlin SELF,  32 level,  0 lecture
Locked
Token mask
Token mask
1
Task
Kotlin SELF,  32 level,  0 lecture
Locked
Hidden utility
Hidden utility
Комментарии
  • популярные
  • новые
  • старые
Для того, чтобы оставить комментарий Вы должны sign in
This page has no comments yet