Item: Considere agregar elementos a um mapa
Não é incomum ter um conjunto maior de elementos que precisamos acessar muitas vezes. Pode ser:
- Um cache – dados que baixamos de algum serviço e depois mantemos na memória local para acessá-los mais rapidamente
- Um repositório com dados carregados de algum arquivo
- Um repositório na memória usado para diferentes tipos de testes
Esses dados podem representar alguma lista de usuários, ids, configurações, etc. Eles geralmente são fornecidos a nós como uma lista, e é tentador representá-los em nossa memória da mesma maneira:
class NetworkUserRepo(val userService: UserService): UserRepo { private var users: List<User>? = null override fun getUser(id: UserId): User? { if(users == null) { users = userService.getUsers() } return users?.firstOrNull { it.id == id } } } class ConfigurationsRepository( val configurations: List<Configuration> ) { fun getByName(name: String) = configurations .firstOrNull { it.name == name } }class InMemoryUserRepo: UserRepo { private val users: MutableList<User> = mutableListOf() override fun getUser(id: UserId): User? = users.firstOrNull { it.id == id } fun addUser(user: User) { user.add(user) } }
Essa raramente é a melhor maneira de armazenar esses elementos. Observe como os dados que carregamos são usados. Na maioria das vezes, acessamos um elemento por algum identificador ou nome (ele está conectado à forma como projetamos os dados para ter chaves exclusivas em bancos de dados). Encontrar um elemento em uma lista tem complexidade O(n), onde n é o tamanho da lista. Mais concretamente, leva em média n/2 comparações para encontrar um elemento. É especialmente problemático para listas maiores. Uma boa solução para esse problema é usar um mapa em vez de uma lista. Por padrão, o Kotlin usa um Map hash (concretamente LinkedHashMap) e, conforme descrevemos no Item 41: Respeite o contrato de hashCode, o desempenho de encontrar um elemento quando usamos o mapa hash é muito melhor. Na verdade, na JVM, graças ao fato de que o tamanho do hash map usado é ajustado ao tamanho do próprio mapa, se o hashCode for implementado corretamente, encontrar um elemento deve levar apenas uma comparação.
Este é InMemoryRepo usando um mapa em vez de uma lista:
class InMemoryUserRepo: UserRepo { private val users: MutableMap<UserId, User> = mutableMapOf() override fun getUser(id: UserId): User? = users[id] fun addUser(user: User) { user.put(user.id, user) } }
A maioria das outras operações, como modificar ou iterar sobre esses dados (provavelmente usando métodos de processamento de coleção como filtro, mapa, flatMap, classificado, soma, etc.) têm mais ou menos o mesmo desempenho para a lista e mapa padrão.
A questão é como nos transformamos de uma lista em um mapa e vice-versa. Para isso usamos funções associadas. O mais comum é o associatedBy que constrói um mapa onde os valores são os elementos da lista e as chaves são produzidas na expressão lambda:
data class User(val id: Int, val name: String) val users = listOf(User(1, "Michal"), User(2, "Marek")) val byId = users.associateBy { it.id } byId == mapOf(1 to User(1, "Michal"), 2 to User(2, "Marek")) val byName = users.associateBy { it.name } byName == mapOf("Michal" to User(1, "Michal"), "Marek" to User(2, "Marek"))
Observe que as chaves nos mapas devem ser exclusivas ou as duplicatas serão removidas. É por isso que devemos apenas associar por um identificador único (para agrupar por algo que não é único, use groupBy).
val users = listOf(User(1, "Michal"), User(2, "Michal")) val byName = users.associateBy { it.name } byName == mapOf("Michal" to User(2, "Michal"))
Para transformar o contrário, você pode usar valores:
val users = listOf(User(1, "Michal"), User(2, "Michal")) val byId = users.associateBy { it.id } users == byId.values
É assim que os outros repositórios podem ser implementados usando um mapa para melhorar o desempenho de acesso aos elementos:
class NetworkUserRepo(val userService: UserService): UserRepo { private var users: Map<UserId, User>? = null override fun getUser(id: UserId): User? { if(users == null) { users = userService.getUsers().associateBy { it.id } } return users?.get(id) } } class ConfigurationsRepository( configurations: List<Configuration> ) { val configurations: Map<String, Configuration> = configurations.associateBy { it.name } fun getByName(name: String) = configurations[name] }
Essa técnica é importante, mas não é para todos os casos. É mais útil quando frequentemente precisamos acessar esses elementos. É por isso que é especialmente importante no backend, onde essas coleções podem ser acessadas muitas vezes por segundo. Não é tão importante no frontend (com isso quero dizer também Android ou iOS), onde um usuário acessará este repositório no máximo algumas vezes. Também precisamos lembrar que o mapeamento de uma lista para um mapa também leva algum tempo, portanto, se o fizermos com frequência, pode prejudicar nosso desempenho também.
Obrigado por gastar seu tempo lendo este artigo. Compartilhe se você gostou!