How do I use the "reified" keyword meanwhile do not expose other property which should be privite?

eg:

object ModelFactory {

    val modelMap = HashMap<Class<*>, Model>()

    inline fun <reified T : Model> getModel(): T {

        var model = modelMap[T::class.java]

        if (model == null) {
            model = T::class.java.newInstance()
            modelMap[T::class.java] = model
        }

        return model as T
    }
}

as the above code showing,you need to declare public keyword to the modelMap property,otherwise you can’t use this property in the getModel function. But if I code like this,in other place,I can access the modelMap property easily,this is not I expect. So I want to know whether there are some good ways to solve this problem?

// ------------------------------------ divider ----------------------------
According to the @Wasabi375 and @pdvrieze 's answers,I figure out two solutions。

  1. use @PublishedApi
object ModelFactory {

    @PublishedApi
    internal val modelMap = HashMap<Class<*>, Model>()

    inline fun <reified T : Model> getModel(): T {

        var model = modelMap[T::class.java]

        if (model == null) {
            model = T::class.java.newInstance()
            modelMap[T::class.java] = model
        }

        return model as T
    }
}
  1. user overload
object ModelFactory {
    
    private val modelMap = HashMap<Class<*>, Model>()

    inline fun <reified T : Model> getModel(): T {
        return getModel(T::class.java)
    }

    @Suppress("UNCHECKED_CAST")
    fun <T :Model> getModel(clazz: Class<T>):T{
        var model = modelMap[clazz]

        if (model == null) {
            model = clazz.newInstance()
            modelMap[clazz] = model
        }

        return model as T
    }
}

I prefer the second solution,seemingly it can also be used in Java.

1 Like

modelMap needs to be public because the getModel function is inlined. You can think of it this way: Whenever you call getModel the compiler copies the code instead of calling a function. So

fun foo() {
    val model = getModel<Bar>()
    model.doSomething()
}

gets compiled to

fun foo() {
    // copied code from getModel()
    var _model = modelMap[T::class.java]
    if (_model == null) {
        _model = T::class.java.newInstance()
        modelMap[T::class.java] = _model
    }
    val model = _model as T

    // rest of the foo function
    model.doSomething()
}

That’s why your modelMap property needs to be public. There is however one thing you can do. You can declare it as internal and add the @PublishedAPI annotation to it.

@PublishedAPI 
internal val modelMap

This means kotlin will treat the property as any other internal property but you can still call it from a public inline function.

2 Likes

An alternative option is to have two functions. One which takes the reified generic type, and one which just takes a class parameter. You can then have the reified function call the non-reified, non-inline function. This encapsulates the implementation. You could use @PublishedAPI with internal on the non-reified function, but you don’t really need to. It also makes the api accessible from Java.

4 Likes

Thanks a lot!

This is not necessary. You can also use clazz.cast() for a checked cast:

fun <T: Model> getModel(clazz: Class<T>): T {
    var model = modelMap[clazz]
     if (model == null) {
        model = clazz.newInstance()
        modelMap[clazz] = model
    }
    return clazz.cast(model)
}
1 Like

In this particular case you know that the instance created by newInstance is a valid instance. Invoking clazz.cast will check this (already statically true) fact and therefore introduce some overhead.

1 Like

No, thanks to type erasure I cannot be sure of that. Yes, you can argue that when you make sure that there are no compiler warnings this is safe. Also making sure that everyone accessing modelMap is doing it right. And assuming you are not using reflection. Many libraries may do though. And if you have an error in your code, it will make it a lot easier to trace it, when you do a checked cast, because you get a much better pointer to the source of your problem.

Of course you are right that this introduces a slight performance penalty and if that counts, an unsafe cast is an option. Just want to mention that the statement that this is safe is not universally true.

Actually the cast method is also erased. It doesn’t verify type parameters either. The only thing is that clazz.cast has the casting embedded inside its implementation, but it isn’t any safer/correct.

Actually the cast method is also erased. It doesn’t verify type parameters either. The only thing is that clazz.cast has the casting embedded inside its implementation, but it isn’t any safer/correct.

I think you misunderstand what I mean. Here is some code:

@Test(expectedExceptions = [ClassCastException::class])
fun castUncheckedShouldThrow() {
    castUnchecked(String::class.java, Any())
}

@Test(expectedExceptions = [ClassCastException::class])
fun castCheckedShouldThrow() {
    castChecked(String::class.java, Any())
}

private fun <T> castChecked(clazz: Class<T>, value: Any): T = clazz.cast(value)
private fun <T> castUnchecked(clazz: Class<T>, value: Any): T = value as T

The test castUncheckedShouldThrow fails, but castCheckedShouldThrow is passes. I guess what you mean is, that if the class we cast to has yet another type parameter (like List<String>) then, the cast is unchecked either way. However at least for basic types the cast is checked.

My point is that given a class value of runtime type Class<T> in a variable clazz, the semantics of newInstance and cast are such that clazz.newInstance will create a value whose runtime type is T. clazz.cast will take any value and check (as much as possible considering erasure - it will not warn) that that value has a runtime type T.

In this particular case model is of type Any. However, we know (looking at the code) that in reality the type will always be that of T (if it is null it will be initialized with such value). As such using an (unchecked) cast is correct, although using the cast method will do some additional checking (perhaps catching -at runtime- future errors due to incorrect modifications). It probably depends on the specific use case which direction to go. For library/generic code I would go with the “unchecked” version, but carefully audit and isolate the code (including using the inline reified version only to call a non-inline function that does the work, don’t make the map internal or published, make it private).

1 Like