In this playground, I overview how to access classes within packages where you may not know the name of said package at runtime.
Firstly, what’s a playground? As far as my blog goes, it’s an informal tutorial. Nothing more, nothing less. It just dives right into the content. It’s a tutorial that I write in bed with a cup of tea in an hour or 2 (or 3) as opposed to a full-fledged tutorial that can take around a week of researching and thorough proofing.
I plan to show how to access classes from package names which may not be determined at runtime efficiently. Normally, in this situation, you’d access them via reflection. The reason for this is that reflection is bad. There’s no denying it. Reflection is insecure (Unless you have a suitable security manager in place), it’s “hacky” and can cause lots of unexpected errors and overall, it’s a massive performance liability. Sure, reflection may not be completely unavoidable, but there’s a way to optimize the amount of reflective calls that are used.
The reflection way
This week, I’ve been rewriting the majority of my CommandAPI project due to the release of Minecraft 1.14. During the rewriting process, after a long discussion with a friend, I decided to remove reflection to boost performance and improve the process of updating the project to new versions of Minecraft in the future. It so happens that I used reflection because I could not determine the name of a package at compile time… that is, until I used this sneaky little trick to avoid reflection and use the package names I want.
Say you’re using some library in a Java project. It has its classes in the package some.library.v1
and you want to access a class SomeClass
. This is pretty trivial:
1
2
3
import some.library.v1.SomeClass;
SomeClass instance = new SomeClass(); //Instantiate it for example
However, the worst thing you can possible imagine happens and they change the package name to have v2
instead of v1
. Now, say you are also going to distribute your code to users that may be running version 1 or version 2 of this library (you don’t know which one). The easiest thing to do is then to create two copies of your project, one for library version 1 and one for library version 2. But then you have to keep track of the changes between the two projects and that becomes a massive pain. Of course, this is easily solved with reflection:
1
2
3
4
5
6
7
8
9
String[] versions = new String[] {"v1", "v2"};
String packageName = null;
for(String version : versions) {
try {
Class.forName("some.library." + version + ".SomeClass");
packageName = "some.library." + version + ".";
} catch(ClassNotFoundException e) {}
}
Now, whenever you want to access SomeClass
, regardless of what version the target user is using, you can refer to the class using Class.forName(packageName + "SomeClass")
. Yeah, it may be a bit slow, so perhaps you cache it for future use (And of course, if you’re caching classes, you may as well be caching methods, fields and constructors that you may use while you’re at it).
It’s not that bad, is it? For example, say we wanted to run the following code:
1
2
3
4
5
6
7
8
SomeClass instance = new SomeClass();
instance.addPerson("bob");
try {
instance.getPerson("bob");
} catch(PersonNotFoundException e) {
System.out.println("bob not found :(");
}
In reflection, that’s an absolute nightmare. You have a constructor and two method invocations, one which throws a PersonNotFoundException. At the very least, your reflection implementation would look something like:
1
2
3
4
5
6
7
8
9
10
11
12
13
Class<?> someClass = Class.forName(packageName + "SomeClass");
Object instance = someClass.newInstance();
someClass.getDeclaredMethod("addPerson", String.class).invoke(instance, "bob");
try {
someClass.getDeclaredMethod("getPerson", String.class).invoke(instance, "bob");
} catch(InvocationTargetException e) {
if(Class.forName(packageName + "PersonNotFoundException").isInstance(e.getCause())) {
System.out.println("bob not found :(");
}
}
And that’s omitting the tonne of security, instantiation and invocation exceptions (not to forget method and class not found exceptions) that would arise from using reflection.
Surely there’s a better way?
Version independent interfaces
Say we have some class that runs the code we want. For now, we make it compatible with library version 1. Let’s call this class HandlerV1
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import some.library.v1.SomeClass;
public class HandlerV1 {
public void runCode() {
SomeClass instance = new SomeClass();
instance.addPerson("bob");
try {
instance.getPerson("bob");
} catch(PersonNotFoundException e) {
System.out.println("bob not found :(");
}
}
}
Okay, that’s good. We can also create a similar class specifically for library version 2, calling this class HandlerV2
:
1
2
3
4
5
import some.library.v2.SomeClass;
public class HandlerV2 {
//Code omitted - it's the same as runCode() above
}
Now we want to run one of these handlers… but how do we know which one to run? Well, to make things easier, let’s make an interface which can be called, which will run either of the two implementations we have.
1
2
3
public interface Handler {
public void runCode();
}
Now we let our handlers implement this interface:
1
2
3
4
5
6
7
public class HandlerV1 implements Handler {
//Code omitted
}
public class HandlerV2 implements Handler {
//Code omitted
}
So if we have some instance Handler
, which is either HandlerV1
or HandlerV2
, we can run them just by interacting with the interface, which is version independent. We already know how to access the version of the class we need, using that snippet of reflection above which gives us packageName
. Sure, it’s still reflection, but it’s better to have one intensive reflection call than to keep invoking methods each time you want to run runCode()
, right?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
//Get the current version that the user is running
String[] versions = new String[] {"v1", "v2"};
String currentVersion = null;
for(String version : versions) {
try {
Class.forName("some.library." + version + ".SomeClass");
currentVersion = version;
} catch(ClassNotFoundException e) {}
}
//Instantiate Handler
Handler instance = null;
switch(version) {
case "v1":
instance = new HandlerV1(); break;
case "v2":
instance = new HandlerV2(); break;
default:
//Handle this as you see fit
throw new Exception("Unsupported library version");
break;
}
//Run our code!
instance.runCode();
And it’s as simple as that. You’ve got your instance of a class, it’s already declared for the specific library version that the user is running and you’ve cut down the amount of reflection you would have used. This implementation gives you all of the benefits of not having reflection:
- It’s fast. You only have to resolve 1 class name (You can do this at the start of your program so it doesn’t affect anything anywhere) and you only have to do this once.
- It’s reliable. You’re using the imported packages from the library - you know the method names and field names are valid. You know what exceptions are going to be thrown and these are handled as you would normally handle them.
- It makes upgrading a breeze. All you have to do is add a new Handler version (e.g.
HandlerV3
), change the imported package names and add a new element to a switch case. This makes dealing with potential version content changes a joy to fix. In other words, if in a new version, a method was renamed or a field was removed, the compiler will flag this up.