Kotlin is a cross-platform, statically typed, general-purpose programming language with type inference. Developed by JetBrains, it is designed to interoperate fully with Java, but provides more concise syntax and addresses many of Java's pain points, such as "null pointer exceptions." In 2019, Google announced Kotlin as its preferred language for Android app developers.
- Interoperability: You can call Java code from Kotlin and vice-versa seamlessly within the same project.
- Null Safety: The compiler systematically helps avoid
NullPointerExceptionsby distinguishing between nullable and non-nullable types. - Conciseness: It reduces boilerplate code significantly (e.g., Data Classes replace dozens of lines of Java getters/setters).
- Extension Functions: Allows you to extend a class with new functionality without having to inherit from the class.
- Coroutines: Provides a way to write asynchronous, non-blocking code that is easy to read and maintain.
- Smart Casts: The compiler automatically tracks checks and performs type casts for you.
-
val(Value): Used for immutable variables.Once assigned, the value cannot be changed (similar tofinalin Java). -
var(Variable): Used for mutable variables.The value can be reassigned later in the program.
Kotlin's type system is aimed at eliminating the "Billion Dollar Mistake" of null references.
-
Non-Nullable: By default, variables cannot hold null.
var name: String = "Alex"will throw a compile error if you try to set it tonull. -
Nullable: To allow a variable to hold null, you must explicitly append a
?to the type. -
Safe Call Operator (
?.): Executes a method only if the variable is not null.
A Data Class is a concise way to create classes that are primarily used to hold data. When you declare a class as data, the compiler automatically generates standard methods like equals(), hashCode(), toString(), and copy().
Kotlin allows you to declare a primary constructor class header itself. It is the most common way to initialize a class.
Extension functions allow you to "add" methods to existing classes without modifying their source code. For example, you can add a method to the standard String class:
The Elvis Operator is used to provide a default value when an expression results in null.
Kotlin provides an intuitive way to work with ranges using the .. and step keywords, making loops very readable.
Lambdas are "function literals"—functions that are not declared but passed immediately as an expression. They are often used for high-order functions like filtering or mapping collections.
While both val and const val represent immutable values, they differ in when they are initialized and where they can be used.
| Feature | val |
const val |
|---|---|---|
| Initialization | Determined at runtime. | Determined at compile-time. |
| Mutability | Read-only; can be assigned a function result. | Truly constant; must be a primitive or String. |
| Placement | Can be inside functions, classes, or top-level. | Only at top-level or inside an object / companion object. |
| Performance | Access involves a getter method call. | Inlined by the compiler (faster access). |
Example Usage:
-
const valis used for hardcoded values that never change such as (API keys or mathematical constants). -
valis used for values that remain constant after being calculated during the application's execution.
The init Block in Kotlin
The init block is an initializer block used to run setup logic or validate parameters immediately after an instance of a class is created.It acts as a bridge for logic that cannot be placed directly within the primary constructor declaration.
- Execution Timing: It executes immediately after the primary constructor is called and before any secondary constructors are executed.
- Property Initialization: It runs in the order it appears in the class body, interspersed with property initializers.
- Access: It has direct access to the parameters defined in the primary constructor.
Comparison: Primary Constructor vs. init Block
| Feature | Primary Constructor | init Block |
|---|---|---|
| Purpose | Declares properties and defines entry point. | Executes logic/code (loops, validation, logging). |
| Code Execution | Cannot contain code blocks. | Can contain any valid Kotlin code. |
| Quantity | Only one per class. | Multiple init blocks allowed (run top-to-bottom. |
Code Example:
Inheritance in Kotlin
In Kotlin, inheritance allows a class (subclass) to inherit properties and methods from another class (superclass). By design, Kotlin favors composition over inheritance, leading to a stricter default behavior than Java.
-
The
openKeyword: All classes and methods in Kotlin arefinalby default, meaning they cannot be inherited or overridden unless explicitly marked with theopenkeyword. -
The
overrideKeyword: Subclasses must use theoverridemodifier when redefining a method or property from the superclass. - Single Inheritance: A class can only inherit from one superclass (but can implement multiple interfaces).
-
Syntax: The colon (
:) is used for both inheritance and interface implementation.
Why Classes are Final by Default
Kotlin follows the design principle "Design and document for inheritance or else prohibit it," as popularized by Joshua Bloch in Effective Java.
| Reason | Impact |
|---|---|
| Fragile Base Class Problem | Prevents developers from accidentally breaking subclasses when changing the internal logic of a base class. |
| Encapsulation | Ensures that a class’s behavior is predictable and cannot be altered externally without the author's permission. |
| Performance | Allows the compiler to perform optimizations (like inlining) because it knows a method's implementation cannot be swapped out via a subclass. |
| API Stability | Forces library authors to be intentional about which parts of their code are intended for extension. |
Example Implementation:
The open Keyword in Kotlin
In Kotlin, the open keyword is the opposite of final. It explicitly permits a class or a member (method/property) to be subclassed or overridden. Since Kotlin classes are closed by default to ensure robustness, you must use open to enable inheritance.
-
For Classes: Applying
opento a class header allows other classes to inherit from it. -
For Members: Applying
opento a property or function within anopenclass allows subclasses to provide a custom implementation using theoverridekeyword.
When to Use open
You should use the open keyword specifically when you intend to design a class for extension. Common scenarios include:
| Scenario | Application |
|---|---|
| Abstract Base Logic | When creating a base class that holds shared logic but requires specialization (e.g., a BaseActivity in Android). |
| Template Method Pattern | When a class defines a fixed algorithm but allows certain steps (marked open) to be modified by subclasses. |
| Testing/Mocking | When using certain mocking frameworks (like Mockito) that require classes to be non-final to create "mock" versions of them. |
| UI Components | When building a library where users need to extend your views or components to add custom behavior. |
Example:
Abstract Classes vs. Interfaces in Kotlin
In Kotlin, both Abstract Classes and Interfaces are used to define contracts for subclasses. However, they differ significantly in their structural capabilities and how they store state.
| Feature | Abstract Class | Interface |
|---|---|---|
| State (Properties) | Can store state (can have instance fields/backing fields). | Cannot store state (no backing fields allowed). |
| Constructor | Can have constructors with parameters. | Cannot have constructors. |
| Inheritance | A class can inherit from only one abstract class. | A class can implement multiple interfaces. |
| Default Behavior | Can have both abstract and fully implemented methods. | Can have abstract methods or methods with default implementations. |
| Visibility | Members can be protected or internal. |
Members are always public (though can be private in some contexts). |
When to Use Which
-
Use Abstract Classes when you want to share a common base of code and state among closely related classes (e.g., a
Vehiclebase class that stores afuelLevelproperty). -
Use Interfaces when you want to define a common behavior (a "can-do" relationship) across unrelated classes (e.g., both a
Userand aDocumentmight implementPrintable).
Code Example:
Implementing Multiple Interfaces
In Kotlin, a class can implement any number of interfaces. This is the primary way to achieve polymorphism across different behavioral sets without the limitations of single inheritance.
-
Syntax: List the interfaces separated by commas after the colon
(
:). - Conflict Resolution: If two interfaces provide a default implementation for a method with the same name, the subclass must override that method to resolve the ambiguity.
-
Super Calls: To call a specific interface's implementation
within
the override, use the
super< InterfaceName >.methodName()syntax.
Handling Method Conflicts
When multiple interfaces share a method signature, Kotlin requires explicit resolution to prevent the "Diamond Problem."
| Component | Usage |
|---|---|
| Override | Mandatory if method signatures collide. |
super A |
Directs the compiler to use Interface A's logic. |
super B |
Directs the compiler to use Interface B's logic. |
Code Example:
Sealed Classes in Kotlin
A Sealed Class is used to represent restricted class hierarchies. It allows you to define a set of subclasses that are known at compile-time, ensuring that no other subclasses can be created outside the file where the sealed class is defined.
-
Exhaustive Checks: When used in a
whenexpression, the compiler verifies that all possible subclasses are covered, removing the need for anelsebranch. - State Management: Unlike Enums, each subclass of a sealed class can have multiple instances and hold unique data (state).
Sealed Class vs. Enum
| Feature | Enum Class | Sealed Class |
|---|---|---|
| Instance Constraints | Only one instance of each constant (Singleton). | Can have multiple instances of each subclass. |
| Data/State | All constants share the same properties/types. | Each subclass can have different properties and methods. |
| Hierarchy | Flat list of values. | Can be a full hierarchy (subclasses, data classes, or objects). |
| Usage | Best for constant values (Days of week, Colors). | Best for state-machine logic (Success, Error, Loading). |
Code Example:
The object Keyword in Kotlin
In Kotlin, the object keyword is a thread-safe, first-class citizen
used to
define a
Singleton—a class that has exactly one instance. Unlike Java, where
you
must manually
implement the Singleton pattern using private constructors and static methods,
Kotlin
handles the instantiation and lifecycle automatically.
- Initialization: The object is initialized lazily the first time it is accessed.
- Thread Safety: The initialization is thread-safe by default.
- Functionality: Objects can inherit from classes and implement interfaces, just like regular classes.
- Global Access: It can be accessed directly by its name without using a constructor.
Use Cases for object
| Type | Purpose |
|---|---|
| Singleton Object | For global utility classes, database managers, or configuration holders. |
| Companion Object | To define members that belong to the class rather than instances
(replaces Java's static). |
| Anonymous Object | Created using object : InterfaceName { ... } to
implement interfaces or override classes on the fly. |
Code Example:
The companion object in Kotlin
A companion object is a specialized object declared inside a class. It
is
used to define
members (properties and methods) that belong to the class itself
rather
than to specific
instances of that class.
In Java, these are called static members. Kotlin does not have the
static keyword;
instead, it uses companion objects to group these members while maintaining a
strictly
object-oriented structure.
-
Access: Members are called using the class name (e.g.,
MyClass.myFunction()). - Interface Support: Unlike Java statics, a companion object can implement interfaces and extend classes.
- Quantity: Only one companion object is allowed per class.
- Access to Private Members: The companion object can access private members of the containing class.
Companion Object vs. Java Static
| Feature | Java static |
Kotlin companion object |
|---|---|---|
| Type | A keyword for members. | A real object instance. |
| Interfaces | Cannot implement interfaces. | Can implement interfaces. |
| Inheritance | Does not support polymorphism. | Can extend other classes. |
| Naming | N/A | Can be named (default is Companion). |
Code Example: Factory Pattern
A common use case for companion objects is the Factory Pattern, where you hide the constructor and provide a static-like method to create instances.
Higher-Order Functions in Kotlin
A Higher-Order Function is a function that does at least one of the following:
- Takes one or more functions as parameters.
- Returns a function as its result.
In Kotlin, functions are "first-class citizens," meaning they can be stored in variables and passed around just like integers or strings. This is a core pillar of functional programming in Kotlin.
Key Components
| Component | Description |
|---|---|
| Function Type | The signature of the function, e.g., (Int, Int) -> Int. |
| Lambda Expression | The actual block of code passed to the function, e.g.,
{ a, b -> a + b }. |
it Keyword |
Implicit name for a single parameter in a lambda. |
Code Examples:
-
Passing a Function as a Parameter
This is commonly used for callbacks or defining custom behavior for a process.
-
Returning a Function
Useful for "function factories" where the output depends on the input configuration.
Common Built-in Higher-Order Functions
Kotlin's standard library uses these extensively for collection processing:
-
.filter { ... }: Takes a predicate function to filter a list. -
.map { ... }: Takes a transformation function to change list items. -
.forEach { ... }: Takes a function to execute for every element.
List vs. MutableList vs. Array
In Kotlin, these three structures are used to store collections of elements, but they differ in mutability, memory representation, and flexibility.
| Feature | Array | List | MutableList |
|---|---|---|---|
| Mutability | Size is fixed; elements can be changed (mutable content). | Immutable; neither size nor elements can be changed. | Mutable; both size and elements can be changed. |
| Memory | Optimized at the JVM level as primitive arrays (e.g.,
int[]).
|
Objects; usually backed by java.util.ArrayList. |
Objects; backed by java.util.ArrayList. |
| Type Safety | Invariant (an Array<String> is not an
Array<Any>).
|
Covariant (a List<String> is a
List<Any>).
|
Invariant (a Mutable MutableList<String> is not
MutableList<Any>).
|
| Performance | Fastest for low-level indexing and primitive storage. | Lightweight for read-only data. | Slight overhead due to resizing logic. |
Key Differences Explained
-
Array<T>: Use this when you know the number of elements in advance and need high-performance access. It is a low-level structure where the size is locked upon initialization. -
List<T>: This is an interface for a read-only collection. You cannotaddorremoveitems. It provides a view of the data that ensures it won't be modified, which is essential for thread safety. -
MutableList<T>: Use this when you need to dynamically grow or shrink your collection. It includes methods like.add(), .remove(),and.clear().
Code Example:
Collection Transformations: Filter, Map, and Reduce
These are higher-order functions used to process collections in a functional style.
They allow you to transform or analyze data without using manual for
loops.
| Function | Purpose | Return Type |
|---|---|---|
filter |
Selects elements that satisfy a specific condition. | A new collection of the same type (or subset). |
map |
Transforms each element into something else. | A new collection of the same size, potentially a different type. |
reduce |
Combines all elements into a single value using an accumulator. | A single value of the same type as the elements. |
Detailed Breakdown
-
filter: Takes a predicate (a function returning aBoolean).If the predicate istrue, the element is included in the new list.-
Example:
numbers.filter { it > 10 }
-
Example:
-
map: Applies a transformation function to every element. This is useful for extracting specific properties from a list of objects.-
Example:
users.map { it.name }(Turns a list of Users into a list of Strings).
-
Example:
-
reduce: Starts with the first element as the "accumulator" and sequentially applies an operation. Note: It throws an exception if the collection is empty.(Usefoldif you need an initial starting value).-
Example:
numbers.reduce { acc, next -> acc + next }
-
Example:
Combined Code Example
This example demonstrates a typical data pipeline:
Sequences vs. Iterables
In Kotlin, both IterableIterable (like List or
Set) and Sequence offer functional
processing (map, filter, etc.), but they differ fundamentally in
evaluation
strategy and performance for large datasets.
-
Iterable(Eager Evaluation): Processes the entire collection at each step. If you chain three operations, Kotlin creates two intermediate lists before the final result. -
Sequence(Lazy Evaluation): Processes elements one by one through the entire chain. No intermediate collections are created. It performs "lazy" computation, meaning nothing happens until a terminal operation (liketoList()orfirst()) is called.
Key Comparison
| Feature | Iterable (List/Set) |
Sequence |
|---|---|---|
| Evaluation | Eager (step-by-step). | Lazy (element-by-element). |
| Intermediate Collections | Created at every step (Memory heavy). | None created (Memory efficient). |
| Order of Execution | Completes filter for all, then map for
all. |
Completes filter + map for element 1, then
element 2... |
| Best For | Small collections or simple transformations. | Large datasets or complex processing chains. |
| Terminal Operation | Not required (executes immediately). | Required to trigger execution. |
Behavioral Example
Consider a list of numbers where we want to find the first squared even number:
In this example, the Sequence is significantly faster because it stops as soon as the condition is met, whereas the Iterable finishes processing the entire list even though only the second element was needed.
Infix Functions in Kotlin
An Infix Function allows you to call a
member function or an extension function using a more natural,
English-like syntax by omitting the dot (.) and parentheses
().
This is commonly used to create Domain Specific Languages (DSLs)
or more readable mathematical operations.
Requirements for an Infix Function
To mark a function with the infix keyword, it must satisfy these three conditions:
- It must be a member function or an extension function.
- It must have exactly one parameter.
-
The parameter must not accept a variable number
of arguments (
vararg) and must not have a default value.
Precedence
Infix functions have lower precedence than
arithmetic operators, type casts, and the
rangeTo operator. For example:
-
1..10 step 2is parsed as(1..10) step 2. -
a + b combinedWith cis parsed as(a + b) combinedWith c.
Code Example: Creating an Infix Function
In this example, we create an extension function
for the String class that allows us to "pair" two
strings using the word onto.
Common Built-in Infix Functions
| Function | Usage | Result |
|---|---|---|
to |
val map = mapOf("A" to 1) |
Creates a Pair object. |
step |
for (i in 1..10 step 2) |
Defines the increment in a loop. |
and/or |
if (a and b) |
Bitwise logical operations. |
The when Expression in Kotlin
The when expression is Kotlin's more powerful, flexible
version of the switch statement found in Java or C.
It matches its argument against all
branches sequentially until a condition is satisfied.
-
Expression vs. Statement: Unlike a
switchstatement,whencan be used as an expression, meaning it can return a value that you can assign to a variable. -
No
breakNeeded: Once a branch is matched, the execution finishes for thatwhenblock. You don't need to addbreakkeywords to prevent "fall-through." -
Exhaustiveness: When used as an expression,
the compiler forces you to cover all possible
cases (often requiring an
elsebranch), making the code safer.
Flexible Branching Logic
when is not limited to constants; it can handle ranges, types, and
logic.
| Logic Type | Syntax Example |
|---|---|
| Multiple Values | 0, 1 -> "Binary" |
| Ranges | in 1..10 -> "Small number" |
| Type Checking | is String -> "It's a text" |
| Negation | !in 10..20 -> "Outside range" |
| Function Result | s.toInt() -> "Matches integer" |
Code Example
- As an Expression (Assigning to a Variable)
-
Without an Argument
(Replacing
if-else if)If no argument is supplied, the branch conditions act as simple boolean expressions.