Kotlin4Noobs

Buscar nas notas

    <T>

    Genéricos

    Classes e funções genéricas, restrições e variância.

    Generics permitem escrever código flexível e reutilizável que funciona com diferentes tipos mantendo type safety. Eles são fundamentais para criar coleções, classes e funções que podem trabalhar com qualquer tipo de forma segura.

    Classes genéricas

    Uma classe genérica é declarada com parâmetros de tipo entre < >:

    class Box<T>(val value: T) {
        fun get(): T = value
    }
    
    val intBox = Box(123)        // Box<Int>
    val stringBox = Box("Hello") // Box<String>

    Você pode ter múltiplos parâmetros de tipo:

    class Pair<A, B>(val first: A, val second: B) {
        fun swap(): Pair<B, A> = Pair(second, first)
    }

    Exemplo completo:

    class Container<T>(private var item: T) {
        fun get(): T = item
        
        fun set(newItem: T) {
            item = newItem
        }
        
        fun <R> transform(transformer: (T) -> R): Container<R> {
            return Container(transformer(item))
        }
    }
    
    fun main() {
        // Container de String
        val stringContainer = Container("Hello")
        println(stringContainer.get())  // Hello
        
        stringContainer.set("World")
        println(stringContainer.get())  // World
        
        // Transformando String em Int (tamanho)
        val intContainer = stringContainer.transform { it.length }
        println(intContainer.get())  // 5
        
        // Container de Person
        data class Person(val name: String, val age: Int)
        val personContainer = Container(Person("Gustavo", 25))
        
        // Transformando Person em String
        val nameContainer = personContainer.transform { it.name }
        println(nameContainer.get())  // Gustavo
    }

    Funções genéricas

    Funções podem ter seus próprios parâmetros de tipo:

    fun <T> singletonList(item: T): List<T> {
        return listOf(item)
    }
    
    fun <T> T.print(): T {
        println(this)
        return this
    }

    Exemplo completo:

    fun <T> swap(list: MutableList<T>, index1: Int, index2: Int) {
        val temp = list[index1]
        list[index1] = list[index2]  
        list[index2] = temp
    }
    
    fun <T> List<T>.middle(): T? {
        return if (isEmpty()) null else this[size / 2]
    }
    
    fun <T, R> List<T>.mapNotNull(transform: (T) -> R?): List<R> {
        val result = mutableListOf<R>()
        for (item in this) {
            transform(item)?.let { result.add(it) }
        }
        return result
    }
    
    fun main() {
        // Swap genérico
        val numbers = mutableListOf(1, 2, 3, 4, 5)
        swap(numbers, 1, 3)
        println(numbers)  // [1, 4, 3, 2, 5]
        
        val names = mutableListOf("Ana", "Bruno", "Carlos")
        swap(names, 0, 2)
        println(names)  // [Carlos, Bruno, Ana]
        
        // Extension function genérica
        println(numbers.middle())  // 3
        println(names.middle())    // Bruno
        
        // Transformação com null safety
        data class Person(val name: String, val age: Int)
        val people = listOf(
            Person("João", 25),
            Person("Maria", 17),
            Person("Pedro", 30)
        )
        
        val adults = people.mapNotNull { person ->
            if (person.age >= 18) person.name else null
        }
        println(adults)  // [João, Pedro]
    }

    Constraints

    Você pode restringir os tipos que podem ser usados como argumentos genéricos:

    // T deve ser um subtipo de Number
    fun <T : Number> List<T>.sum(): Double {
        var sum = 0.0
        for (element in this) {
            sum += element.toDouble()
        }
        return sum
    }
    
    // T deve implementar Comparable
    fun <T : Comparable<T>> max(first: T, second: T): T {
        return if (first > second) first else second
    }

    Múltiplas restrições com where:

    fun <T> process(item: T) 
        where T : CharSequence,
              T : Comparable<T> {
        // T deve ser CharSequence E Comparable
    }

    Exemplo completo:

    // Interface para exemplo
    interface Identifiable {
        val id: Int
    }
    
    // Classes de exemplo
    data class Person(
        override val id: Int,
        val name: String,
        val age: Int
    ) : Identifiable, Comparable<Person> {
        override fun compareTo(other: Person): Int = age.compareTo(other.age)
    }
    
    data class Product(
        override val id: Int,
        val name: String,
        val price: Double
    ) : Identifiable
    
    // Função com constraint simples
    fun <T : Identifiable> findById(list: List<T>, id: Int): T? {
        return list.find { it.id == id }
    }
    
    // Função com múltiplas constraints
    fun <T> getOldest(list: List<T>): T? 
        where T : Identifiable,
              T : Comparable<T> {
        return list.maxOrNull()
    }
    
    // Classe genérica com constraint
    class Cache<T : Identifiable>(private val maxSize: Int) {
        private val items = mutableListOf<T>()
        
        fun add(item: T) {
            if (items.size >= maxSize) {
                items.removeAt(0)
            }
            items.add(item)
        }
        
        fun get(id: Int): T? = items.find { it.id == id }
        
        fun getAll(): List<T> = items.toList()
    }
    
    fun main() {
        val people = listOf(
            Person(1, "João", 25),
            Person(2, "Maria", 30),
            Person(3, "Pedro", 22)
        )
        
        val products = listOf(
            Product(1, "Notebook", 3000.0),
            Product(2, "Mouse", 50.0)
        )
        
        // Usando função com constraint
        println(findById(people, 2))     // Person(id=2, name=Maria, age=30)
        println(findById(products, 1))   // Product(id=1, name=Notebook, price=3000.0)
        
        // Usando função com múltiplas constraints
        println(getOldest(people))       // Person(id=2, name=Maria, age=30)
        // getOldest(products)           // Erro: Product não implementa Comparable
        
        // Usando classe genérica com constraint
        val cache = Cache<Person>(2)
        cache.add(Person(1, "Ana", 20))
        cache.add(Person(2, "Bruno", 25))
        cache.add(Person(3, "Carlos", 30))  // Remove Ana (FIFO)
        
        println(cache.getAll())  // [Bruno, Carlos]
    }

    Variância

    Kotlin usa in e out para declarar variância:

    • out (covariância): pode produzir T, mas não consumir
    • in (contravariância): pode consumir T, mas não produzir
    // Producer - só pode retornar T (covariante)
    interface Producer<out T> {
        fun produce(): T
    }
    
    // Consumer - só pode receber T (contravariante)  
    interface Consumer<in T> {
        fun consume(item: T)
    }
    
    // Invariante - pode tanto produzir quanto consumir
    interface Storage<T> {
        fun store(item: T)
        fun retrieve(): T
    }

    Exemplo completo:

    open class Person(val name: String)
    open class Employee(name: String, val id: Int) : Person(name)  // Added 'open' keyword
    class Manager(name: String, id: Int, val team: String) : Employee(name, id)
    
    // Covariância (out) - Producer
    class PersonProducer<out T : Person>(private val person: T) {
        fun get(): T = person
        // fun set(person: T) {}  // Erro! Não pode consumir T
    }
    
    // Contravariância (in) - Consumer
    class PersonConsumer<in T : Person> {
        fun process(person: T) {
            println("Processando: ${person.name}")
        }
        // fun get(): T = ...  // Erro! Não pode produzir T
    }
    
    // Invariância - pode consumir e produzir
    class PersonStorage<T : Person> {
        private var stored: T? = null
        
        fun store(person: T) {
            stored = person
        }
        
        fun retrieve(): T? = stored
    }
    
    fun main() {
        // Covariância (out) permite usar subtipos
        val employeeProducer: PersonProducer<Employee> = PersonProducer(Employee("João", 123))
        val personProducer: PersonProducer<Person> = employeeProducer  // OK!  Employee é subtipo de Person
        println(personProducer.get().name)
        
        // Contravariância (in) permite usar supertipos
        val personConsumer: PersonConsumer<Person> = PersonConsumer()
        val employeeConsumer: PersonConsumer<Employee> = personConsumer  // OK! Person é supertipo de Employee
        employeeConsumer.process(Employee("Maria", 456))
        
        // Invariância não permite nenhuma conversão
        val employeeStorage: PersonStorage<Employee> = PersonStorage()
        // val personStorage: PersonStorage<Person> = employeeStorage  // Erro!
        
        // Exemplo prático com listas
        val employees: List<Employee> = listOf(
            Employee("Ana", 111),
            Manager("Carlos", 222, "Dev Team")
        )
        
        // List é covariante (out), então podemos fazer isso:
        val people: List<Person> = employees
        people.forEach { println("Pessoa: ${it.name}") }
        
        // MutableList é invariante, então isso não compila: 
        // val mutablePeople: MutableList<Person> = mutableListOf(Employee("Test", 999))  // Erro!
    }

    Reified type parameters

    Com inline e reified, você pode acessar o tipo em runtime:

    inline fun <reified T> isInstance(value: Any): Boolean {
        return value is T
    }
    
    inline fun <reified T> filterByType(list: List<Any>): List<T> {
        return list.filterIsInstance<T>()
    }

    Exemplo completo:

    open class Person(val name: String)
    class Employee(name: String, val id: Int) : Person(name)
    class Customer(name: String, val points: Int) : Person(name)
    
    // Função inline com reified
    inline fun <reified T> createInstance(): String {
        return when (T:: class) {
            String::class -> "Criando String" as T
            Int::class -> 42 as T
            Person::class -> Person("Default") as T
            else -> throw IllegalArgumentException("Tipo não suportado:  ${T::class}")
        } as String
    }
    
    inline fun <reified T : Person> findPeople(people: List<Person>): List<T> {
        return people.filterIsInstance<T>()
    }
    
    inline fun <reified T> List<*>.countType(): Int {
        return count { it is T }
    }
    
    inline fun <reified T> safeCast(value: Any): T? {
        return value as? T
    }
    
    // Função que cria um mapa agrupando por tipo
    inline fun <reified T> List<Any>.groupByType(): Map<String, List<T>> {
        val items = filterIsInstance<T>()
        return mapOf(T::class.simpleName.orEmpty() to items)
    }
    
    fun main() {
        val mixedList: List<Person> = listOf(
            Person("João"),
            Employee("Maria", 123),
            Customer("Pedro", 500),
            Employee("Ana", 456),
            Customer("Carlos", 1000)
        )
        
        // Filtrando por tipo específico
        val employees = findPeople<Employee>(mixedList)
        println("Funcionários:  ${employees.map { it.name }}")
        
        val customers = findPeople<Customer>(mixedList)
        println("Clientes: ${customers.map { it.name }}")
        
        // Contando tipos
        val anyList: List<Any> = listOf("text", 123, true, "more text", 456, Person("Test"))
        println("\nContagem por tipo:")
        println("Strings: ${anyList.countType<String>()}")
        println("Inteiros: ${anyList.countType<Int>()}")
        println("Booleanos: ${anyList.countType<Boolean>()}")
        println("Pessoas: ${anyList.countType<Person>()}")
        
        // Safe cast com reified
        val value: Any = "Kotlin"
        val asString = safeCast<String>(value)
        val asInt = safeCast<Int>(value)
        
        println("\nSafe casting:")
        println("Como String: $asString")
        println("Como Int: $asInt")
        
        // Agrupando por tipo
        val grouped = anyList.groupByType<String>()
        println("\nStrings agrupadas: $grouped")
    }

    Generics com data classes

    Generics são muito úteis com data classes para criar estruturas reutilizáveis:

    data class Result<T>(
        val data: T? = null,
        val error: String? = null
    ) {
        val isSuccess: Boolean get() = data != null
        val isError: Boolean get() = error != null
    }
    
    data class PagedList<T>(
        val items: List<T>,
        val page: Int,
        val totalPages: Int
    )

    Exemplo completo:

    // Result genérico para operações que podem falhar
    sealed class Result<out T> {
        data class Success<T>(val data: T) : Result<T>()
        data class Error(val exception: Exception) : Result<Nothing>()
        
        inline fun <R> map(transform: (T) -> R): Result<R> {
            return when (this) {
                is Success -> Success(transform(data))
                is Error -> this
            }
        }
        
        fun getOrNull(): T? {
            return when (this) {
                is Success -> data
                is Error -> null
            }
        }
        
        // Use @UnsafeVariance annotation to allow T in input position
        fun getOrDefault(default: @UnsafeVariance T): T {
            return when (this) {
                is Success -> data
                is Error -> default
            }
        }
    }
    
    // Data class genérica para respostas paginadas
    data class PagedResponse<T>(
        val items: List<T>,
        val currentPage: Int,
        val totalPages: Int,
        val totalItems: Int
    ) {
        val hasNext: Boolean get() = currentPage < totalPages
        val hasPrevious: Boolean get() = currentPage > 1
    }
    
    // Data class para par chave-valor genérico
    data class KeyValue<K, V>(val key: K, val value: V)
    
    // Serviço que usa generics
    class PersonService {
        private val people = listOf(
            Person("João", 25),
            Person("Maria", 30),
            Person("Pedro", 22),
            Person("Ana", 28),
            Person("Carlos", 35)
        )
        
        fun findById(id: Int): Result<Person> {
            return if (id in people.indices) {
                Result.Success(people[id])
            } else {
                Result.Error(NoSuchElementException("Person not found"))
            }
        }
        
        fun getPaged(page: Int, pageSize: Int): PagedResponse<Person> {
            val start = (page - 1) * pageSize
            val end = minOf(start + pageSize, people.size)
            
            return PagedResponse(
                items = if (start < people.size) people.subList(start, end) else emptyList(),
                currentPage = page,
                totalPages = (people.size + pageSize - 1) / pageSize,
                totalItems = people.size
            )
        }
        
        fun getStatistics(): List<KeyValue<String, Any>> {
            return listOf(
                KeyValue("total", people.size),
                KeyValue("averageAge", people.map { it.age }. average()),
                KeyValue("youngest", people.minBy { it.age }),
                KeyValue("oldest", people.maxBy { it.age })
            )
        }
    }
    
    data class Person(val name: String, val age: Int)
    
    fun main() {
        val service = PersonService()
        
        // Usando Result<T>
        val result1 = service.findById(1)
        when (result1) {
            is Result.Success -> println("Encontrado: ${result1.data}")
            is Result.Error -> println("Erro: ${result1.exception.message}")
        }
        
        // Mapeando Result
        val nameResult = service.findById(0).map { it.name.uppercase() }
        println("Nome em maiúsculas: ${nameResult.getOrDefault("DESCONHECIDO")}")
        
        // Usando PagedResponse<T>
        val page1 = service.getPaged(1, 2)
        println("\nPágina ${page1.currentPage} de ${page1.totalPages}:")
        page1.items.forEach { println("  - $it") }
        println("Tem próxima?  ${page1.hasNext}")
        
        // Usando KeyValue<K, V>
        println("\nEstatísticas:")
        service.getStatistics().forEach { stat ->
            println("  ${stat.key}: ${stat.value}")
        }
    }