0
0
Kotlinprogramming~15 mins

Visibility modifiers (public, private, internal, protected) in Kotlin - Deep Dive

Choose your learning style9 modes available
Overview - Visibility modifiers (public, private, internal, protected)
What is it?
Visibility modifiers in Kotlin control where classes, functions, and properties can be accessed from. They help organize code by limiting or allowing access to parts of a program. The main modifiers are public, private, internal, and protected, each defining different access levels. This keeps code safe and easier to maintain.
Why it matters
Without visibility modifiers, all parts of a program would be open to everyone, causing confusion and mistakes. Imagine a library where all books are piled together without sections; it would be hard to find or protect important information. Visibility modifiers prevent accidental changes and help programmers work together without breaking each other's code.
Where it fits
Before learning visibility modifiers, you should understand Kotlin basics like classes, functions, and properties. After mastering visibility, you can explore advanced topics like encapsulation, inheritance, and modular programming, which rely on controlling access to code parts.
Mental Model
Core Idea
Visibility modifiers are like gates that control who can enter and use parts of your code.
Think of it like...
Think of a house with rooms: some rooms are open to all guests (public), some are locked and only family can enter (private), some rooms are shared with close friends (protected), and some are accessible only to people living in the neighborhood (internal).
┌───────────────┐
│   Public      │  ← Everyone can access
├───────────────┤
│  Internal     │  ← Same module/neighborhood
├───────────────┤
│ Protected     │  ← Subclasses/family
├───────────────┤
│  Private      │  ← Only inside this class/room
└───────────────┘
Build-Up - 7 Steps
1
FoundationUnderstanding Public Visibility
🤔
Concept: Public means anything marked public can be accessed from anywhere in the program.
In Kotlin, public is the default visibility. If you declare a class, function, or property as public, it can be used from any other code, even outside the module or package. Example: class Car { fun drive() { println("Driving") } } fun main() { val car = Car() car.drive() // Allowed because drive() is public }
Result
The drive() function can be called from anywhere, showing 'Driving' on the screen.
Knowing public is the default helps avoid unnecessary code and keeps your API open where needed.
2
FoundationPrivate Visibility Basics
🤔
Concept: Private means only the code inside the same class or file can access the member.
When you mark something private, it hides it from other classes or files. This protects internal details. Example: class BankAccount { private var balance = 0 fun deposit(amount: Int) { balance += amount } fun getBalance() = balance } fun main() { val account = BankAccount() account.deposit(100) println(account.getBalance()) // Prints 100 // account.balance = 500 // Error: balance is private }
Result
You can deposit and get balance, but cannot access balance directly from outside.
Private keeps sensitive data safe inside the class, preventing accidental changes.
3
IntermediateInternal Visibility Explained
🤔Before reading on: do you think internal members are accessible outside the module or only inside? Commit to your answer.
Concept: Internal means the member is visible anywhere inside the same module but hidden outside it.
A module is a set of Kotlin files compiled together, like a library or app part. Example: internal class Helper { fun assist() = println("Assisting") } fun main() { val helper = Helper() helper.assist() // Allowed inside the module } // Outside this module, Helper is not visible.
Result
Helper and its functions can be used anywhere in the module but not from other modules.
Internal helps organize code by sharing within a module but hiding from the outside world.
4
IntermediateProtected Visibility and Inheritance
🤔Before reading on: do you think protected members are accessible from any class or only subclasses? Commit to your answer.
Concept: Protected means the member is visible inside its class and any subclass, but not outside those.
Protected is useful when you want to allow child classes to use or change something but keep it hidden from others. Example: open class Animal { protected fun sound() = println("Some sound") } class Dog : Animal() { fun bark() { sound() // Allowed because Dog inherits Animal println("Bark") } } fun main() { val dog = Dog() dog.bark() // dog.sound() // Error: sound is protected }
Result
Dog can call sound() inside bark(), but outside code cannot call sound() directly.
Protected supports safe extension by subclasses without exposing internals to everyone.
5
IntermediateVisibility Modifiers on Top-Level Declarations
🤔
Concept: Visibility modifiers also apply to functions and properties declared outside classes, affecting their accessibility.
In Kotlin, you can declare functions or properties directly in a file (top-level). Example: private fun secret() = println("Secret") internal val version = "1.0" fun publicInfo() = println("Public info") fun main() { publicInfo() // Always accessible secret() // Accessible only inside this file println(version) // Accessible inside the module }
Result
secret() is hidden outside the file, version is hidden outside the module, publicInfo() is accessible everywhere.
Understanding top-level visibility helps organize utility functions and constants properly.
6
AdvancedCombining Visibility with Packages and Modules
🤔Before reading on: do you think package structure affects visibility modifiers directly or only module boundaries? Commit to your answer.
Concept: Kotlin visibility modifiers interact with package and module boundaries to control access precisely.
Public and private are straightforward, but internal depends on modules, not packages. Packages group files logically but do not restrict access by themselves. Example: // In module A, package com.example internal class ModuleClass {} // In module B, even if package is com.example, ModuleClass is invisible. Private restricts to file or class, regardless of package. Protected allows subclass access even across packages if inheritance exists.
Result
Internal hides members outside the module, package names do not grant or restrict access by themselves.
Knowing module boundaries matter more than packages prevents confusion about what code can access.
7
ExpertVisibility and Inline Functions with Reified Types
🤔Before reading on: do you think visibility modifiers affect inline functions with reified types differently? Commit to your answer.
Concept: Inline functions with reified type parameters can expose or hide type information depending on visibility, affecting API design and binary compatibility.
Inline functions are copied into call sites, and reified types allow type checks at runtime. If an inline function is public, its implementation and type details become part of the API. Marking such functions internal or private hides these details, reducing API surface. Example: inline fun printType() { println(T::class.simpleName) } // If public, callers see this function and its type info. // If internal/private, it's hidden from outside. This affects library design and binary compatibility when updating code.
Result
Visibility controls not just access but also what type information is exposed in compiled code.
Understanding this prevents accidental API leaks and helps maintain clean, stable libraries.
Under the Hood
Kotlin's compiler enforces visibility by generating bytecode with access flags and package structures. Public members have no restrictions, private members get restricted access flags limiting visibility to the class or file. Internal members use module boundaries defined by the compiler to restrict access, often implemented via package-private or special metadata. Protected members are marked to allow subclass access, even across packages, by the JVM's protected access rules. The compiler checks visibility at compile time, preventing illegal access before running the program.
Why designed this way?
Visibility modifiers were designed to balance flexibility and safety. Public is open for API use, private protects internals, internal supports modularization, and protected enables inheritance safely. Kotlin's design reflects modern software needs for modularity and encapsulation, improving on Java by adding internal and clearer rules. Alternatives like package-private in Java were less precise, so Kotlin chose explicit modifiers to avoid confusion.
┌───────────────┐
│   Public      │  ← No restrictions, visible everywhere
├───────────────┤
│  Internal     │  ← Visible inside module only
├───────────────┤
│ Protected     │  ← Visible in class and subclasses
├───────────────┤
│  Private      │  ← Visible only inside class/file
└───────────────┘

Compiler checks access at compile time → Generates bytecode with access flags → JVM enforces access at runtime
Myth Busters - 4 Common Misconceptions
Quick: Does 'private' in Kotlin always mean only inside the class? Commit to yes or no.
Common Belief:Private means the member is only accessible inside the class where it is declared.
Tap to reveal reality
Reality:Private at the top-level means accessible only inside the file, not just the class. Inside classes, private means inside the class, but outside classes, private restricts to the file.
Why it matters:Misunderstanding this can cause unexpected access errors or expose code unintentionally, leading to bugs or security issues.
Quick: Is 'internal' the same as 'package-private' in Java? Commit to yes or no.
Common Belief:Internal is like Java's package-private, restricting access to the same package.
Tap to reveal reality
Reality:Internal restricts access to the same module, which can include multiple packages, unlike Java's package-private which is limited to a single package.
Why it matters:Confusing these can lead to wrong assumptions about code visibility, causing design mistakes in modular projects.
Quick: Can protected members be accessed by any class in the same package? Commit to yes or no.
Common Belief:Protected members are accessible by any class in the same package.
Tap to reveal reality
Reality:In Kotlin, protected members are accessible only within the class and its subclasses, regardless of package.
Why it matters:Assuming package-wide access can lead to accidental exposure or misuse of protected members.
Quick: Does marking a function private inside a class prevent subclasses from accessing it? Commit to yes or no.
Common Belief:Private members can be accessed by subclasses because they belong to the class hierarchy.
Tap to reveal reality
Reality:Private members are not accessible by subclasses; only protected and public members are.
Why it matters:Misunderstanding this can cause subclass code to fail unexpectedly, breaking inheritance logic.
Expert Zone
1
Internal visibility depends on the module system, which can be complex in multi-module projects or when using Gradle with multiple source sets.
2
Protected visibility in Kotlin differs from Java by not allowing package-wide access, which can surprise developers coming from Java backgrounds.
3
Top-level private declarations restrict visibility to the file, enabling encapsulation at the file level, a feature not present in many other languages.
When NOT to use
Avoid using public visibility for internal implementation details; prefer private or internal to reduce API surface. Do not use protected unless you expect inheritance; otherwise, private is safer. For multi-module projects, rely on internal to hide code between modules. If you need finer control, consider explicit interfaces or composition instead of visibility tricks.
Production Patterns
In production, libraries expose only public APIs, keep helpers internal, and use private for sensitive data. Protected is used carefully in inheritance hierarchies to allow extension without exposing internals. Internal is heavily used in modular apps to separate features and prevent accidental dependencies. Top-level private functions organize file-specific utilities without polluting the module API.
Connections
Encapsulation in Object-Oriented Programming
Visibility modifiers implement encapsulation by controlling access to data and behavior.
Understanding visibility modifiers deepens the grasp of encapsulation, which protects object integrity and hides complexity.
Access Control in Operating Systems
Both use rules to restrict who can access resources, whether files or code members.
Seeing visibility as access control helps appreciate its role in security and stability in software systems.
Modular Design in Architecture
Visibility modifiers support modular design by defining clear boundaries and interfaces between parts.
Knowing this connection helps design software that is easier to build, test, and maintain, just like well-planned buildings.
Common Pitfalls
#1Trying to access a private property from outside its class.
Wrong approach:class Person { private val ssn = "123-45-6789" } fun main() { val p = Person() println(p.ssn) // Error: ssn is private }
Correct approach:class Person { private val ssn = "123-45-6789" fun getSsn() = ssn } fun main() { val p = Person() println(p.getSsn()) // Allowed }
Root cause:Misunderstanding that private members cannot be accessed outside their class.
#2Assuming internal members are accessible across modules with the same package name.
Wrong approach:// Module A internal class Helper {} // Module B (same package) fun test() { val h = Helper() // Error: Helper is internal to Module A }
Correct approach:// Use public or provide API in Module A to expose Helper functionality public class Helper {} fun test() { val h = Helper() // Allowed }
Root cause:Confusing module boundaries with package boundaries.
#3Using protected members from unrelated classes in the same package.
Wrong approach:open class Base { protected fun greet() = println("Hello") } class Other { fun test() { val b = Base() b.greet() // Error: greet is protected } }
Correct approach:open class Base { protected fun greet() = println("Hello") } class Derived : Base() { fun test() { greet() // Allowed } }
Root cause:Misunderstanding that protected means package-wide access instead of subclass-only.
Key Takeaways
Visibility modifiers control who can see and use parts of your code, helping keep it safe and organized.
Public means open to all, private hides inside classes or files, internal limits to modules, and protected allows subclasses access.
Understanding module boundaries is key to using internal correctly, as it is not tied to packages.
Protected visibility is subclass-only, not package-wide, which differs from some other languages.
Using the right visibility modifier prevents bugs, accidental misuse, and helps maintain clean, stable code.