Kotlin Advanced Features
1. Generics: PECE & Type Erasure
Type Erasure
Java (and Kotlin) Generics exist only at compile time. At runtime, List<String> and List<Int> are effectively just List.
- Consequence: You cannot check
if (x is List<String>)(except for reified inline functions). - Star Projection:
box: Box<*>means "A box of something, but we don't know what." It's the safe way to read generic objects when the type is erased.
Variance (PECS / PECE)
This is about Producer extends, Consumer super.
out T(Covariant): You effectively say "This class produces T".- Since it produces
T, it can produceT's children. List<out Animal>: You can getAnimalout. You cannot put anything in (because it might actually be aList<Cat>, and putting aDogin would break it).
- Since it produces
in T(Contravariant): "This class consumes T".Comparable<in Dog>: It can eat aDog(or any Animal). You cannot get aDogout.
2. Sealed Classes vs Interfaces
The "Exhaustive" Guarantee
When you use a sealed class in a when expression, the compiler checks if you covered all cases.
- How? The compiler tags the class metadata as sealed.
- Benefit: If you add a new error type
NetworkErrorto your sealed class, the compiler breaks the build everywhere you handled the errors, forcing you to handle the new case. This is compiler-driven stability.
Sealed Interface vs Sealed Class
- Sealed Class: Can hold state (constructor parameters) and common logic. Single inheritance (you can only extend one class).
- Sealed Interface: Cannot hold state. Multiple inheritance (a class can implement multiple sealed interfaces).
- Use Case: An object needs to be both
ClickError(UI event) andLoggableError(Analytics). It can implement both sealed interfaces.
- Use Case: An object needs to be both
3. Inline Functions & Reification
The Bytecode Cost
Inline functions are powerful but dangerous.
inline fun run(block: () -> Unit)
- Good: The
blocklambda object is not created. The code insideblockis pasted into theruncall site. - Bad: If
runis 50 lines of code and you call it 1000 times, your app size increases by 50 * 1000 lines of bytecode. - Rule: Inline only small functions that take lambdas.
Reified Types: Breaking Erasure
inline fun <reified T> isType(value: Any) = value is T
- Magic: Since the function body is copied,
Tis known at the call site. - Call:
isType<String>(var)-> Compiler pastes:var is String. - This is the only way to break Java's type erasure limitations.
4. Extension Functions: Static Resolution
Static Dispatch
fun Shape.draw()
If you have val s: Shape = Circle() and call s.draw(), which one is called?
- Extension Function: It calls
Shape.draw. Extensions are resolved by the variable type, not the runtime object. - Member Function: It calls
Circle.draw. Members are virtual. - Conflict: Member functions always win. If
Circlehas adraw()method, the extension is ignored.
Scope Functions Guide
The choice depends on (1) Do you need to return the object? (2) Do you want to refer to it as this or it?
apply(this -> this): "Apply these settings." Configuration.also(it -> this): "And also do this." Side effects (logging) without breaking the chain.let(it -> result): "Let us compute something." Null checks (?.let).run(this -> result): "Run this block." Computation.
5. Data Classes: Constraints
The copy() Limitation
copy() acts as a comprehensive "clone with modifications".
- Shallow Copy: If a data class contains a
MutableList,copy()copies the reference, not the list contents. Modifying the list in the copy modifies the original!- Best Practice: Data classes should contain only immutable data (
valandList, notMutableList).
- Best Practice: Data classes should contain only immutable data (
Inheritance
Data classes cannot fail inheritance correctly.
- They are
finalby default. - If they could extend a class,
equals()would break (Symmetry violation inequalscontract is hard to solve with inheritance). - However, they can extend interfaces or sealed classes (which is the standard Modeling pattern in Kotlin).
6. Delegation (The by keyword)
Kotlin natively supports the Delegation Pattern, favoring composition over inheritance.
Class Delegation: Automatic Forwarding
Instead of manually forwarding every method call to an inner object, the compiler does it for you.
interface Base { fun print() }
class BaseImpl(val x: Int) : Base {
override fun print() = print(x)
}
// Derived IS-A Base. But it delegates all Base methods to the 'b' object
class Derived(b: Base) : Base by b
// Under the hood, the compiler generates:
// class Derived(private val b: Base) : Base {
// override fun print() = b.print() // Forwarding method
// }
Why? Great for the Decorator Pattern. You can override just one method (e.g., to add logging) and let the compiler delegate the rest.
Property Delegation
Delegate the logic of get() and set() to another class.
lazy: Thread-safe initialization. Value is computed on first access and cached.observable: React to property changes (e.g., for logging).- Custom Delegates: The real power. You can encapsulate complex logic like reading from SharedPreferences or arguments.
Example: SharedPreferences Delegate
class StringPreference(val ctx: Context, val key: String, val default: String)
: ReadWriteProperty<Any, String> {
override fun getValue(thisRef: Any, property: KProperty<*>): String {
return ctx.getSharedPreferences("prefs", MODE_PRIVATE).getString(key, default)!!
}
override fun setValue(thisRef: Any, property: KProperty<*>, value: String) {
ctx.getSharedPreferences("prefs", MODE_PRIVATE).edit().putString(key, value).apply()
}
}
// Usage
var userToken by StringPreference(context, "token", "")
7. Performance Optimizations
Value Classes (JvmInline)
Wrappers are good for type safety, but excessive object allocation hurts the Garbage Collector.
- Problem:
class Password(val s: String)creates a new heap object for every password. - Solution:
@JvmInline value class Password(val s: String). - Under the Hood: The compiler erases the wrapper class and uses the underlying primitive/String directly in bytecode, inserting static utility methods for any logic you defined.
- Limitation: No identity equality (
===doesn't make sense since it's just a value).
Sequences vs Iterables
Crucial for processing large data sets.
- Iterable (
List): Horizontal Processing. Steps are executed for the entire collection before moving to the next step.list.map { .. }.filter { .. }- Step 1: Create new List with mapped values. (Allocates O(N) memory)
- Step 2: Create another new List with filtered values.
- Sequence (
Sequence): Vertical Processing. Each element goes through the entire chain one by one.list.asSequence().map { .. }.filter { .. }- Element 1 -> map -> filter -> Result. (No intermediate collection created)
- Lazy: If you have
take(5)at the end, it stops processing after the 5th result, even if the source has 1,000,000 items.
Golden Rule: Use Sequences for large lists or multi-step chains. Use Iterables for small lists (Sequences have slight overhead).
8. Advanced Functions
inline
- What: Copies the function body to the call site.
- Why: Lambdas are objects. Creating one adds GC pressure. Inlining removes that object creation.
- Cost: Code bloat. If the inline function is huge and called everywhere, your APK size grows.
noinline & crossinline
noinline: "Exclude this specific lambda from inlining." Needed if you pass the lambda to another function that isn't inline (e.g., storing it in a variable).crossinline: "This lambda will run in another context (thread/scope), so you cannot return from the outer function."- Standard inline lambdas allow non-local returns (
returnexits the calling function). crossinlineforbids this because it might run after the calling function has already returned.
- Standard inline lambdas allow non-local returns (
Kotlin Contracts
Contracts allow library developers to share "knowledge" with the compiler.
returns(true) implies (x != null): "If I return true, you can safely assume x is not null."callsInPlace(block, EXACTLY_ONCE): "I promise I will call this lambda exactly once." (Allows initializing variables inside the lambda).
9. Destructuring & Operators
Destructuring Declarations
Allows unpacking: val (name, age) = user.
Under the Hood:
The compiler looks for functions named component1(), component2(), etc.
class User(val name: String, val age: Int) {
operator fun component1() = name
operator fun component2() = age
}
Data classes generate these automatically.
Operator Overloading
Kotlin allows providing implementations for a predefined set of operators (+, -, *, !, [], etc.).
It does not allow defining custom operators (like ~= or -->).
a + b->a.plus(b)a[i]->a.get(i)a()->a.invoke()(Allows calling an object like a function!)
Quick Reference
| Topic | Key Points |
|---|---|
| Delegation | by keyword; Composition over inheritance; lazy properties |
| Performance | value class for zero-overhead wrappers; Sequence for large chains |
| Inline | crossinline blocks non-local returns; reified preserves types |
| Contracts | Teach compiler about smart casts (e.g. isNullOrEmpty) |
| Sealed Classes | Exhaustive state modeling; powerful with when |
| Data Classes | Auto-generated componentN, copy, equals; usage for Destructuring |