blog > Java method references are dangerous (06 Oct 2018)

Java method references are dangerous

This post is the result of me having to figure out a rather strange problem. May the same not happen to you.

Index #

Scenario #

Let's say you have a class TheClass with a method theMethod that does something important.

In a later version, you noticed that theMethod can be used elsewhere too so you move it to BaseClass and make TheClass extend BaseClass.

$ cat versionA/TheClass.java
class TheClass {
	void theMethod() {
		System.out.println("hi!");
	}
}

$ cat versionB/TheClass.java
class BaseClass {
	void theMethod() {
		System.out.println("hi!");
	}
}

class TheClass extends BaseClass {
}

TheClass.java for both versions.

Index

Calling the method #

Now, what if TheClass is part of a library and a user wants to use it? They can invoke theMethod in a few different ways.

$ cat User.java
public class User {
	public static void main(String[] args) {
		TheClass i = new TheClass();
		i.theMethod();
		((Runnable) () -> i.theMethod()).run();
		((Runnable) i::theMethod).run();
	}
}

Usage of theMethod in other code.

Let's try compiling and running User.java using TheClass from version A.

$ javac versionA/TheClass.java
$ javac -cp versionA/ User.java
$ java -cp ".;versionA/" User
hi!
hi!
hi!

Using version A.

Let's do the same for version B.

$ javac versionB/TheClass.java
$ javac -cp versionB/ User.java
$ java -cp ".;versionB/" User
hi!
hi!
hi!

Using version B.

All great, everything works as expected.

Index

Mixing versions on the classpath #

But what happens if there's a different version of the library containing TheClass on the running classpath? Let's see.

Mixing versions on the classpath

Compiling for version A and running with version B #

$ javac -cp versionA/ User.java
$ java -cp ".;versionB/" User
hi!
hi!
hi!

All good!

Index

Mixing versions on the classpath

Compiling for version B and running with version A #

$ javac -cp versionB/ User.java
$ java -cp ".;versionA/" User
hi!
hi!
Exception in thread "main" java.lang.BootstrapMethodError: java.lang.NoClassDefFoundError: BaseClass
  at User.main(User.java:6)
Caused by: java.lang.NoClassDefFoundError: BaseClass
  ... 1 more
Caused by: java.lang.ClassNotFoundException: BaseClass
  at java.net.URLClassLoader.findClass(Unknown Source)
  at java.lang.ClassLoader.loadClass(Unknown Source)
  at sun.misc.Launcher$AppClassLoader.loadClass(Unknown Source)
  at java.lang.ClassLoader.loadClass(Unknown Source)
  ... 1 more

Uh-oh! The normal invocation and lambda are working fine, but when it's trying to call someMethod using a method reference, things suddenly explode.

This can be quite confusing for the user of the library (me, yesterday). We're getting a ClassNotFoundException for BaseClass on line 6, but we don't even know where BaseClass came from. Only TheClass is used and there's no BaseClass to be found in the library that we compiled against.

Index

Investigating the crashsite #

Let's see what there is to be found under the hood in the bytecode.

Investigating the crashsite

Normal invocation #

// (omitted)

 aload1
 invokevirtual TheClass.theMethod()V

// (omitted)

Nothing special for the normal invocation.

Index

Investigating the crashsite

Lambda invocation #

// (omitted)

 aload1

 invokedynamic java/lang/invoke/LambdaMetafactory.metafactory(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodHandle;Ljava/lang/invoke/MethodType;)Ljava/lang/invoke/CallSite; : run(LTheClass;)Ljava/lang/Runnable; ()V User.lambda$main$0(LTheClass;)V (6) ()V
 invokeinterface java/lang/Runnable.run()V

// (omitted)

 private static synthetic lambda$main$0(TheClass arg0) { //(LTheClass;)V
  L1
   aload0
   invokevirtual TheClass.theMethod()V
   return
 }

Lots of more code for the lamba invocation, but in the end it's adding a method that calls TheClass.theMethod like a normal invocation.

Index

Investigating the crashsite

Method reference invocation #

 aload1
 dup
 invokevirtual java/lang/Object.getClass()Ljava/lang/Class;
 pop
 invokedynamic java/lang/invoke/LambdaMetafactory.metafactory(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodHandle;Ljava/lang/invoke/MethodType;)Ljava/lang/invoke/CallSite; : run(LTheClass;)Ljava/lang/Runnable; ()V BaseClass.theMethod()V (5) ()V
 invokeinterface java/lang/Runnable.run()V

Method reference + invocation.

Well hello there. Inside that big mess there's BaseClass.theMethod()V. Of course this won't work when having version A on the classpath.

So what happens when with the method reference it's compiled against version A?

 aload1
 dup
 invokevirtual java/lang/Object.getClass()Ljava/lang/Class;
 pop
 invokedynamic java/lang/invoke/LambdaMetafactory.metafactory(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodHandle;Ljava/lang/invoke/MethodType;)Ljava/lang/invoke/CallSite; : run(LTheClass;)Ljava/lang/Runnable; ()V TheClass.theMethod()V (5) ()V
 invokeinterface java/lang/Runnable.run()V

Method reference + invocation when compiled using version A.

No real surprises here actually, now it just uses TheClass.theMethod. But this also works fine when running it while version B is on the classpath.

Index

In the real world #

A real world situation can be that you're writing an extension or plugin for a host application. The code hierarchy in the host application may change in newer versions, but the changes might not be directly visible (imagine theMethod being deeper down in the structure). Of course you'd want your plugin to still work for previous versions. It compiles fine when using the latest version so you'd think it's all good, but then problems like this could appear when a user uses an older version of the host application (that used to work with your extension/plugin).

In this situation, if one still wants a single binary for different versions of the host application, one can either not use method references where they cause issues like this or just compile using the older version of the host's SDK.

Index