Imagine that we have a Function<A, B> foo = SomeClass::someMethod;
object Function<A, B> foo = SomeClass::someMethod;
This is a lambda, which is guaranteed to be a link to a non-static method. How can one get from the foo
object an instance of the Method
class corresponding to the written method?
If in short, then in no way, information about a specific method is stored exclusively in bytecode (I do not take into account all the instrumentation there). But this does not prevent us in certain cases to get what we want to bypass.
So, our goal is a method:
static <A, B> Method unreference(Function<A, B> foo) { //... }
which could be used as follows:
Method m = unreference(SomeClass::someMethod)
The first thing to start with is to look directly at the class to which the method belongs. That is, for the Function<A, B>
you need to find a specific A
Usually, if we have a parameterized type and its concrete implementation, we can find the values of the parameter types by calling getGenericSuperClass()
on the specific implementation. For good, this method should return us an instance of the ParameterizedType
class, which is already provided by an array of specific types via the getActualTypeArguments()
call.
Type genericSuperclass = foo.getClass().getGenericSuperclass(); Class actualClass = (Class) ((ParameterizedType) genericSuperclass).getActualTypeArguments()[0];
But with lambdas such a trick does not work - runtime for the sake of simplicity and efficiency scores on these details and in fact gives us an object of type Function<Object, Object>
(in such a situation there is no need to generate bridge methods and all sorts of metadata). GenericSuperclass for it coincides with just the superclass, and the above code will not work.
For us, this is certainly not the best news, but all is not lost, since the implementation of apply (or any other "functional" method) for lambdas looks like this:
public Object apply(Object param) { SomeClass cast = (SomeClass) param; return invokeSomeMethod(cast); }
It is this cast that we need, since this is one of the few places that actually stores information about the type (the second such place is the method called in the next line). What happens if I apply an object of the wrong class to apply? True, ClassCastException
.
try { foo.apply(new Object()); } catch (ClassCastException e) { //... }
The message in our exception will look like this: java.lang.Object cannot be cast to sample.SomeClass
(at least in the tested version of the JRE, the specification on the subject of this message says nothing). Unless this is a method of the Object class, then we cannot guarantee anything, alas.
Knowing the name of the class, we can easily get the corresponding instance of the class Class
. Now it remains to get the name of the method. This is already more difficult, since information about the method, as mentioned earlier, is only in bytecode. If we had our own copy of the class SomeClass, whose method calls we could track, then we could pass it to apply and see what caused it (calling the stack of calls or something else).
The first thing that comes to my mind is, of course, java.lang.reflect.Proxy
, but it introduces a new and very strong limitation - SomeClass must be an interface so that we can generate a proxy for it. On the other hand, the code thus turns out to be absolutely elementary - we create a proxy, call apply
and inside the invoke
method of our InvocationHandler
object we get a ready-made Method
, which we wanted to find!
The full code looks like this. I naturally do not recommend using it in real projects.
private static final Pattern classNameExtractor = Pattern.compile("cannot be cast to (.*)$"); public static <A, B> Method unreference(Function<A, B> reference) { Function erased = reference; try { erased.apply(new Object()); } catch (ClassCastException cce) { Matcher matcher = classNameExtractor.matcher(cce.getMessage()); if (matcher.find()) { try { Class<?> iface = Class.forName(matcher.group(1)); if (iface.isInterface()) { AtomicReference<Method> resultHolder = new AtomicReference<>(); Object obj = Proxy.newProxyInstance(ClassLoader.getSystemClassLoader(), new Class[]{iface}, (proxy, method, args) -> { resultHolder.set(method); return null; } ); try { erased.apply(obj); } catch (Throwable ignored) { } return resultHolder.get(); } } catch (ClassNotFoundException ignored) { } } } throw new RuntimeException("Something's wrong"); }
You can check it like this (a separate variable is needed because Java does not always cope with type inference):
Function<List, Integer> size = List::size; System.out.println(unreference(size));
This code will correctly execute and output public abstract int java.util.List.size()
.
Source: https://habr.com/ru/post/311788/
All Articles