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 produce T's children.
    • List<out Animal>: You can get Animal out. You cannot put anything in (because it might actually be a List<Cat>, and putting a Dog in would break it).
  • in T (Contravariant): "This class consumes T".
    • Comparable<in Dog>: It can eat a Dog (or any Animal). You cannot get a Dog out.

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 NetworkError to 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) and LoggableError (Analytics). It can implement both sealed interfaces.

3. Inline Functions & Reification

The Bytecode Cost

Inline functions are powerful but dangerous. inline fun run(block: () -> Unit)

  • Good: The block lambda object is not created. The code inside block is pasted into the run call site.
  • Bad: If run is 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, T is 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 Circle has a draw() 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 (val and List, not MutableList).

Inheritance

Data classes cannot fail inheritance correctly.

  • They are final by default.
  • If they could extend a class, equals() would break (Symmetry violation in equals contract 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 (return exits the calling function).
    • crossinline forbids this because it might run after the calling function has already returned.

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

results matching ""

    No results matching ""