kotlin

Checking if a class exists at runtime while allowing for compile-time/static analysis errors


I'm working with a platform where a few things might or might not be available at runtime. To check for their existence, I have a lazy property that goes (Path is only used here as an example):

import java.nio.file.Path

val pathIsAvailable by lazy {
    try {
        Path::class
        true
    } catch (_: NoClassDefFoundError) {
        false
    }
}

Once compiled, the class reference disappears:

  private static final boolean pathIsAvailable_delegate$lambda$0();
       0: nop
       1: iconst_1
       2: istore_0
       3: goto          9
       6: astore_1
       7: iconst_0
       8: istore_0
       9: iload_0
      10: ireturn

One way to fix it is to assign the reference to a variable (e.g., val foo = Path::class):

  private static final boolean pathIsAvailable_delegate$lambda$0();
       0: nop
       1: ldc           #26                 // class java/nio/file/Path
       3: astore_0
       4: iconst_1
       5: istore_0
       6: goto          12
       9: astore_1
      10: iconst_0
      11: istore_0
      12: iload_0
      13: ireturn

However, I'm not sure if this is good enough; the variable is also unused and thus it can also be optimized away.

How to check if class exists somewhere in package? recommends using Class.forName(), but it would only cause runtime errors and not compile-time/static analysis ones, which is half the purpose.

Using both is a choice, but that sure won't look pretty. Plus, the aforementioned optimization doesn't seem to be applied when the reference is for a companion object (e.g., kotlin.text.Regex).

Is there a better way, one that is less likely to be affect by optimizations and at the same time subjected to static analysis? I'm using IntelliJ IDEA; fewer warnings is more desirable, though I don't mind suppressing one or two of them.


Solution

  • If you think the compiler is likely to optimise an away something like val foo = Path::class, pass the class literal to a function

    fun Any?.discard() {}
    

    Then in lazy { ... }

    Path::class.discard()
    

    The compiler is very unlikely to go inside the implementation of discard and see that it does nothing. If you are still worried, put discard in a totally different module.


    If you want a guarantee from a specification, I recommend using Java instead.

    // call this from 'lazy { ... }'
    public static void checkPathExists() {
        var x = Path.class;
    }
    

    JLS 15.8.2 states in detail how a class literal is evaluated:

    A class literal evaluates to the Class object for the named class, interface, array type, or primitive type (or for void), as defined by the defining class loader (§12.2) of the class of the current instance.

    It can be inferred that evaluating a class literal requires the class to be loaded by some class loader. "As defined by" could be considered as referring to ClassLoader.defineClass. This will cause a NoClassDefFoundError to be thrown (as per §12.2.1) if the class loader cannot find the class.

    Finally, according to §14.4.2, the execution of a local variable declaration statement involves evaluating the initialiser expression, so Path.class is guaranteed to be evaluated.


    Note that this is assuming that the class has not been loaded before. A class loader could be implemented to load Path before you access pathIsAvailable.