Avoiding IllegalAccessError when using Kotlin
TL;DR: be careful when using SAM conversions with Kotlin
(Note: The following scenarios were tested in Android Studio 2.3.2, I’m not sure if it works the same in other versions)
Say we want to have a class (could be an adapter, a custom view, etc. ) that can notify us when something happens. To handle that scenario, we’ll create an interface where we define a single event, a setter that allows us to attach a listener, and a method that will call the listener event when executed:
package com.example.pack.one;
public class MyClass {
interface Listener{
void foo();
}
private Listener mListener;
public void setListener(Listener listener) {
mListener = listener;
}
public void run(){
if(mListener != null){
mListener.foo();
}
}
}
It’s written in Java because, although Kotlin is great, sometimes we just don’t dare Convert Java File to Kotlin File.
For the sake of the example, let’s assume that this file is in a package called com.example.pack.one
Now, lets use this class somewhere else, for example, an Activity on a different package. The Java version would be something like this:
package com.example.pack.two;
// some imports
import com.example.package.one.MyClass;
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_java);
MyClass myClass = new MyClass();
myClass.setListener(new MyClass.Listener() {
@Override
public void foo() {
}
});
}
}
Awesome, right? Not really. If you actually code that in your IDE, you’ll notice an error:
We actually had an error in the first code. The Listener interface had no public / private / protected identifier, so it defaults to package-level access. Luckily, the IDE is giving us an immediate warning. We can easily fix the error setting the interface definition as public, but let’s keep the buggy code for a while.
Now, lets see how the activity would look if we were using Kotlin:
package com.example.pack.two
// some imports
import com.example.pack.one.MyClass
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
val myClass = MyClass()
myClass.setListener(object: MyClass.Listener {
override fun foo() {
}
})
myClass.run()
}
}
But this happens…
No matter how hard we try to fail, the IDE comes to the rescue every time.
But… who ever uses the “object: MyClass.Listener” notation in Kotlin in that situation? When we are implementing interfaces with a single method, we can take advantage of SAM conversions and use a lambda expression instead. Even the IDE will suggest you to do it, because the code looks much better that way! So lets try it.
package com.example.pack.two
// some imports
import com.example.pack.one.MyClass
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
val myClass = MyClass()
myClass.setListener {
//do something
}
myClass.run()
}
}
Now, the code looks cleaner… but the warning is gone. The IDE is no longer complaining, even though that’s an error waiting to happen.
Let’s see what is happening behind the scenes. We’ll use the Show Kotlin Bytecode -> Decompile option (more info here) to see what’s the Java version of our code (irrelevant parts removed):
package com.example.pack.two;
// some imports
import com.example.pack.one.MyClass;
import com.example.pack.one.MyClass.Listener;
public final class MainActivity extends AppCompatActivity {
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
this.setContentView(2130968604);
MyClass myClass = new MyClass();
myClass.setListener((Listener)null.INSTANCE);
myClass.run();
}
}
Try writing that Java code manually, and it won’t work. We are importing a package level interface on a different package, when by definition it shouldn’t be possible. We’ll see why this will be a problem.
Debugging the code
I tried three scenarios and this were the results (code here):
- When testing the debug build on a device with API 21+, the app runs with no problems.
- When testing the release build on a device with API 21+, the app crashes with IllegalAccessError, which makes perfect sense:
- When testing the debug & release builds on a device with < API 21, the app crashed with NoClassDefFoundError:
The exact reasons for why it works on the first scenario are beyond my knowledge. Why it crashes with NoClassDefFoundError on the third scenario also kept me thinking, because if I check the generated .class files, I can find that file. Finally, I think it’s pretty clear why the second scenario crashes.
I’m sure all these have clever explanations, but I haven’t been able to figure them out.
Just fix it and move on …
Any other day, I would’ve fix the error and move on, but I want to share my experience after I run into this bug a few days ago.
Kotlin is an amazing language that Android developers can use to code their apps. Ever since Google announced that they were supporting it, a lot of people jumped into the bandwagon, including me!
Yet, no matter how many people support it and how much everyone loves it, there’s always that little part inside you that wonders “but what if…?” What if it doesn’t work as expected? Can I trust it as much as I trust the good ol’ Java? Will I get new crashes because of this language?
I’ve probably avoided dozens of NullPointerExceptions since adopting Kotlin, but it only takes one little error like this for your team to start having doubts. Having trust issues with a language you’ve just started to adopt is not good. When someone suggests “should we go back to Java and throw away the +100 Kotlin classes we’ve already written?”, you can’t help but panic (even though you know that most likely won’t happen).