Is there any way to get a "safe" `is` check on Kot...
# getting-started
j
Is there any way to get a "safe"
is
check on Kotlin/JVM, in the sense that it doesn't fail with
NoClassDefFoundError
if the class is not on the classpath? I would like to do something with an object if it's of a given type, with the smart cast, but I don't want it to fail if running on lower JDK that doesn't have that class, I just want the condition to be false, and the code to move on without trying to load the class.
I'm currently juggling with the fqn of the class,
Class.forName
and unsafe casts, but I wish there was something baked in
My current use case is about extracting information from an exception, and I have a
when
that handles different types of exceptions. One of them only exists on JDK11+ and the
when
is now failing on Android target because of the missing class
k
Just a thought:
Copy code
catch (e: Exception) {
    when {
        ClassLoader.getSystemResource("java/lang/whatever/ThisParticularException.class") != null
            && e is ThisParticularException -> TODO()
    }
}
j
Yes that's along the lines of what I'm doing now, but using
Class.forName()
with try/catch instead
My latest attempt looks like this:
Copy code
@OptIn(ExperimentalContracts::class)
private inline fun <reified C : Any> Any.safeIs(className: String): Boolean {
    contract {
        returns(true) implies(this@safeIs is C)
    }
    if (!classExists(className)) {
        return false
    }
    checkMatches<C>(className) // prevent developer mistakes
    return this is C
}

private inline fun <reified T : Any> checkMatches(className: String) {
    val typeName = T::class.qualifiedName
    require(typeName == className) {
        "Mismatch between the given class name '$className' and the actual parameter type of the action lambda '$typeName"
    }
}

private fun classExists(className: String): Boolean = try {
    Class.forName(className)
    true
} catch (e: ClassNotFoundException) {
    false
}
So I can have nice usages like:
Copy code
if (obj.safeIs<HttpClient>("java.net.http.HttpClient")) {
    // obj is smart-cast to HttpClient here
}
Which also includes a check that the fqn and the type param actually match
e
when https://youtrack.jetbrains.com/issue/KT-16304/Compile-time-intrinsic-names-shall-be-properly-treated-as-constants is implemented, perhaps it will be possible for
HttpClient::class.name
to be inlined as a compile-time constant
this requires more overhead but in general I'd rather split stuff using potentially-unavailable classes into classes of their own, and use reflection to load those wrappers, instead of using reflection on third-party classes directly
Copy code
internal interface Foo {
    fun isHttpClient(value: Any): Boolean
}
internal class Java11Foo {
    fun isHttpClient(value: Any): Boolean = value is HttpClient
}
fun isHttpClient(value: Any): Boolean {
    val iterator = ServiceLoader.load(Foo::class.java).iterator()
    while (iterator.hasNext()) {
        try {
            return iterator.next().isHttpClient(value)
        } catch (_: NoClassDefFoundError) {
            // continue
        }
    }
    return false
}

// META-INF/services/Foo
Java11Foo