Abstract: Class.getMethods() returns all the public methods in this class and the classes in its ancestry. However, we cannot necessarily call these methods, for example if they are declared in a private inner class or a lambda. In this newsletter we attempt to find all truly public methods.
Welcome to the 273rd edition of The Java(tm) Specialists' Newsletter. My standard treat after dropping the kids at school is a double espresso at Kalathas Beach. The beach is quiet at this time of year and I enjoy sitting there and looking at the wine-dark sea. This morning, we added a bit of spice. My wife and I started with a one mile run in the soft sand, followed by a swim around the islet. Certainly a great way to start the day. The water is still surprisingly warm considering it is already October. The storms will come, as they do every year. But for now, we are thoroughly enjoying Crete.
javaspecialists.teachable.com: Please visit our new self-study course catalog to see how you can upskill your Java knowledge.
Yesterday I tried to find all public methods of an unknown
object. It should have been easy. Find out what the class is
with getClass()
and then call
getMethods()
. This should work even if
we have a SecurityManager
installed. In my
first test, I used a java.util.ArrayList
.
Everything was just dandy. I then refactored the code to use
Arrays.asList()
so that I could create and
initialize the list in one step. Boom, everything exploded
with IllegalAccessException
.
import java.lang.reflect.*; import java.util.*; public class ReflectionPuzzle { public static void main(String... args) { Collection<String> names = new ArrayList<>(); Collections.addAll(names, "Goetz", "Marks", "Rose"); printSize(names); printSize(Arrays.asList("Goetz", "Marks", "Rose")); printSize(List.of("Goetz", "Marks", "Rose")); printSize(Collections.unmodifiableCollection(names)); } private static void printSize(Collection<?> col) { System.out.println("Size of " + col.getClass().getName()); try { Method sizeMethod = col.getClass().getMethod("size"); System.out.println(sizeMethod.invoke(col)); } catch (ReflectiveOperationException e) { System.err.println(e); } } }
When we run it, we see the following output:
Size of java.util.ArrayList 3 Size of java.util.Arrays$ArrayList java.lang.IllegalAccessException: class ReflectionPuzzle cannot access a member of class java.util.Arrays$ArrayList (in module java.base) with modifiers "public" Size of java.util.ImmutableCollections$ListN java.lang.IllegalAccessException: class ReflectionPuzzle cannot access a member of class java.util.ImmutableCollections$ListN (in module java.base) with modifiers "public" Size of java.util.Collections$UnmodifiableCollection java.lang.IllegalAccessException: class ReflectionPuzzle cannot access a member of class java.util.Collections$UnmodifiableCollection (in module java.base) with modifiers "public"
Only the printSize()
with ArrayList
worked.
size()
is a public method defined in
java.util.Collection
. However, the defining class also
has to be public.
When I lamented about this on Twitter,
Brian Goetz picked up on it and explained that I was trying
to do language-level reflection. Instead, I should see it as
class-level reflection. This is true, of course. If I had
written List.class.getMethod("size")
all of the
list objects would have happily volunteered their size.
There is no such thing as a "private" class in Java. The only access a class can have is public or package access. When we define an inner class as "private", it is not marked as private in the class file. Instead it is "package access" and gets special rules for how outer classes can access it. In the past all access was done via synthetic methods, but this changed in Java 12.
Let's define a slightly more complex class model, involving multiple interface inheritance and covariant return types.
We start with interfaces A
and B
,
both defined in the package "problem". They each have method
foo()
, but with different and unrelated return
types.
package problem; public interface A { CharSequence foo(); }
package problem; public interface B { java.io.Serializable foo(); }
Next we define the class Hidden inside another package
problem.inner
. It contains two factory methods.
The first getLambda()
returns a lambda that
implements A
. The second
getPrivateInnerClass()
returns an instance of
C
. Note that C
implements
both A
and B
and that the
return type of foo()
is a class that extends
both of the return types of the two interfaces. We also
have another public method bar()
. Whilst
public, it is not accessible since the inner class is
private.
package problem.inner; import problem.*; public class Hidden { public static A getPrivateInnerClass() { return new C(); } private static class C implements A, B { public String foo() { return "Hello World"; } public String bar() { return "Should not be visible"; } } public static A getMethodClass() { class D implements A { public CharSequence foo() { return "inside method"; } } return new D(); } public static A getLambda() { return () -> "Hello Lambert"; } }
We can see an example of how we could try foo()
using plain ol' reflection:
import problem.*; import problem.inner.*; import java.lang.reflect.*; import java.util.stream.*; public class TestPlainReflection { public static void main(String... args) { System.out.println("Testing private inner class"); test(Hidden.getPrivateInnerClass()); System.out.println(); System.out.println("Testing method inner class"); test(Hidden.getMethodClass()); System.out.println(); System.out.println("Testing lambda"); test(Hidden.getLambda()); } private static void test(A a) { Stream.of(a.getClass().getMethods()) .forEach(System.out::println); printMethodResult(a, "foo"); printMethodResult(a, "bar"); } private static void printMethodResult(Object o, String name) { try { Method method = o.getClass().getMethod(name); System.out.println(method.invoke(o)); } catch (NoSuchMethodException e) { System.out.println("Method " + name + "() not found"); } catch (IllegalAccessException e) { System.out.println("Illegal to call " + name + "()"); } catch (InvocationTargetException e) { throw new IllegalStateException(e.getCause()); } } }
We do not do too well. Whilst finding the method
foo()
with the call to getMethod()
,
we cannot call it since it belongs to the lambda and the
private inner class. Furthermore, the methods shadow
the methods defined inside interfaces A
and
B
. The call to getMethods()
on the
private inner class C
returns three methods
called "foo", all with the same empty parameters, but
different return types. The String foo()
method is the one defined in class Hidden$C
.
the other two are synthetic methods, generated by the
compiler to produce covariant return types.
Testing private inner class public CharSequence problem.inner.Hidden$C.foo() public java.io.Serializable problem.inner.Hidden$C.foo() public String problem.inner.Hidden$C.foo() public String problem.inner.Hidden$C.bar() public Class Object.getClass() public boolean Object.equals(Object) public int Object.hashCode() public String Object.toString() public void Object.wait() throws InterruptedException public void Object.wait(long) throws InterruptedException public void Object.wait(long,int) throws InterruptedException public void Object.notify() public void Object.notifyAll() Illegal to call foo() Illegal to call bar() Testing method inner class public CharSequence problem.inner.Hidden$1D.foo() public Class Object.getClass() public boolean Object.equals(Object) public int Object.hashCode() public String Object.toString() public void Object.wait() throws InterruptedException public void Object.wait(long) throws InterruptedException public void Object.wait(long,int) throws InterruptedException public void Object.notify() public void Object.notifyAll() Illegal to call foo() Method bar() not found Testing lambda public CharSequence problem.inner.Hidden$$Lambda$23/0x67840.foo() public Class Object.getClass() public boolean Object.equals(Object) public int Object.hashCode() public String Object.toString() public void Object.wait() throws InterruptedException public void Object.wait(long) throws InterruptedException public void Object.wait(long,int) throws InterruptedException public void Object.notify() public void Object.notifyAll() Illegal to call foo() Method bar() not found
Note that we could not call foo()
with any
of these objects.
In our Reflections
class, we try discover
methods that are both public and where the enclosing
class is public. To confirm this, we bitwise AND the
modifiers of the method and its enclosing class together.
If the result is still public
, then
we know that this is a truly public method. In the old
reflection mechanism, they throw a NoSuchMethodException
when a method is not found in a particular class. We instead
return an Optional<Method>
.
Our getTrulyPublicMethods()
code recursively
finds all the truly public methods in the class hierarchy.
To simulate shadowing, we only put entries into our
collection if we have not seen a method with the same
signature lower down in the hierarchy. The signature in our
case consists of the return type, the name and the parameter
types. The key we use is a String, made up of these three
elements.
import java.lang.reflect.*; import java.util.*; import java.util.stream.*; public class Reflections { public static Optional<Method> getTrulyPublicMethod( Class<?> clazz, String name, Class<?>... paramTypes) { return getTrulyPublicMethods(clazz) .stream() .filter(method -> matches(method, name, paramTypes)) .reduce((m1, m2) -> { Class<?> r1 = m1.getReturnType(); Class<?> r2 = m2.getReturnType(); return r1 != r2 && r1.isAssignableFrom(r2) ? m2 : m1; }); } public static Collection<Method> getTrulyPublicMethods( Class<?> clazz) { Map<String, Method> result = new HashMap<>(); findTrulyPublicMethods(clazz, result); return List.copyOf(result.values()); } private static void findTrulyPublicMethods( Class<?> clazz, Map<String, Method> result) { if (clazz == null) return; Method[] methods = clazz.getMethods(); for (Method method : methods) { if (isTrulyPublic(method)) result.putIfAbsent(toString(method), method); } for (Class<?> intf : clazz.getInterfaces()) { findTrulyPublicMethods(intf, result); } findTrulyPublicMethods(clazz.getSuperclass(), result); } private static boolean isTrulyPublic(Method method) { return Modifier.isPublic(method.getModifiers() & method.getDeclaringClass().getModifiers()); } private static String toString(Method method) { String prefix = method.getReturnType().getCanonicalName() + method.getName() + " ("; return Stream.of(method.getParameterTypes()) .map(Class::getCanonicalName) .collect(Collectors.joining(", ", prefix, ")")); } private static boolean matches( Method method, String name, Class<?>... paramTypes) { return method.getName().equals(name) && Arrays.equals(method.getParameterTypes(), paramTypes); } }
I am pretty sure that I have not thought of all the possibilities of what could go wrong with the return types and strange class hierarchies. However, it does pass my test in a nice way. Plus it is nicer using Optional than having to catch exceptions. Here is our TestTrulyPublic:
import problem.*; import problem.inner.*; import java.lang.reflect.*; import java.util.*; public class TestTrulyPublic { public static void main(String... args) throws Exception { System.out.println("Testing private inner class"); test(Hidden.getPrivateInnerClass()); System.out.println(); System.out.println("Testing method inner class"); test(Hidden.getMethodClass()); System.out.println(); System.out.println("Testing lambda"); test(Hidden.getLambda()); } private static void test(A a) { Reflections.getTrulyPublicMethods(a.getClass()).forEach( System.out::println); printMethodResult(a, "foo"); printMethodResult(a, "bar"); } private static void printMethodResult( Object o, String methodName) { Optional<Method> method = Reflections.getTrulyPublicMethod( o.getClass(), methodName); method.map(m -> { try { System.out.println("m = " + m); return m.invoke(o); } catch (IllegalAccessException e) { throw new IllegalStateException(e); } catch (InvocationTargetException e) { throw new IllegalStateException(e.getCause()); } }).ifPresentOrElse(System.out::println, () -> System.out.println("Method " + methodName + "() not found")); } }
When we run this second test, we get the following output:
Testing private inner class public abstract java.io.Serializable problem.B.foo() public abstract CharSequence problem.A.foo() public Class Object.getClass() public boolean Object.equals(Object) public int Object.hashCode() public String Object.toString() public void Object.wait() throws InterruptedException public void Object.wait(long) throws InterruptedException public void Object.wait(long,int) throws InterruptedException public void Object.notify() public void Object.notifyAll() m = public abstract java.io.Serializable problem.B.foo() Hello World Method bar() not found Testing method inner class public abstract CharSequence problem.A.foo() public Class Object.getClass() public boolean Object.equals(Object) public int Object.hashCode() public String Object.toString() public void Object.wait() throws InterruptedException public void Object.wait(long) throws InterruptedException public void Object.wait(long,int) throws InterruptedException public void Object.notify() public void Object.notifyAll() m = public abstract CharSequence problem.A.foo() inside method Method bar() not found Testing lambda public abstract CharSequence problem.A.foo() public Class Object.getClass() public boolean Object.equals(Object) public int Object.hashCode() public String Object.toString() public void Object.wait() throws InterruptedException public void Object.wait(long) throws InterruptedException public void Object.wait(long,int) throws InterruptedException public void Object.notify() public void Object.notifyAll() m = public abstract CharSequence problem.A.foo() Hello Lambert Method bar() not found
We see now that foo() is called successfully in all the cases. We also see that bar() is never found.
Kind regards
Heinz
We are always happy to receive comments from our readers. Feel free to send me a comment via email or discuss the newsletter in our JavaSpecialists Slack Channel (Get an invite here)
We deliver relevant courses, by top Java developers to produce more resourceful and efficient programmers within their organisations.