androidkotlinandroid-viewmodeluse-case

by lazy {} causing a cast exception in production / hashmap loosing values after assigning


we have a code that looks somewhat like this small example:

// this is a class that is injected into a view model, it is injected once, as a parameter
// it is used in multiple functions in the view model, some of them may happen one after another or even at once
class UseCaseProvider(repository: Repository) {
    val useCaseMap = HashMap<Class<*>, UseCase<*, *>>()
    private val clearFiltersUseCase by lazy { ClearFiltersUseCase(repository) }
    // maaaany more usecases made in the exact same way...
    
    private fun <INPUT, OUTPUT> register(useCase: UseCase<INPUT, OUTPUT>) {
        useCaseMap[useCase::class.java] = useCase
    }
    
    internal inline <reified T: UseCase<*, *>> get(): T {
        if(!useCaseMap.containsKey(T::class.java)){
            when(T::class) {
                ClearFiltersUseCase -> register(clearFiltersUseCase)
                // maaaany more similar lines
                else -> throw IllegalStateException("UseCase not registered")
            }
        }
        return useCaseMap[T::class.java] as T
    }
}
// example usage in the view model: 
// useCaseProvider.get<ClearFiltersUseCase>().invoke() 
// .invoke() is always suspend

and we get a class cast exception, null being cast into an UseCase class, any ideas why that might be happening?

we have a similar usecase provider in another module, but instead of using lazy & the register function it just creates and saves the instances in a hashmap once the provider is created and it does not crash, thus my suspicion that somehow lazy is at fault here?

I'm accepting any ideas (race conditions for lazy {}, thread-safety were my only ideas, but I wasn't able to reproduce the crash)


Solution

  • @broot was right, but to share more insights on why we got null on a value we just assigned

    (warning: may not be 100% correct, but it seems to explain why this provider crashes and the other one, that assigns each usecase when the provider is created, doesn't):

    When one thread writes to a DYNAMIC SIZE hash map, and another one tries reading from it, IF the hash map has to resize and rehash (done under the hood) it seems it creates a copy with 2x the size of the previous map and fills the keys (with nulls as values) and starts rehashing the values... and if a thread reads at that EXACT time, it will get null as a value from a key that didn't have a null value before

    ways you can fix it:

    1. initialize all objects inside of the hashMap when the Provider is created (no new writes = no resizing needed)
    2. use a ConcurrentHashMap
    3. somehow limit the size of the hashmap (and starting with an inital size that will 100% fit your case)
    4. as we did: remove the hashmap completly :D