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)
@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: