Running on Java 24-ea+24-2960 (Preview)
Home of The JavaSpecialists' Newsletter

273Truly Public Methods

Author: Dr Heinz M. KabutzDate: 2019-10-01Java Version: 13Category: Tips and Tricks
 

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.

Truly Public Methods

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

 

Comments

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)

When you load these comments, you'll be connected to Disqus. Privacy Statement.

Related Articles

Browse the Newsletter Archive

About the Author

Heinz Kabutz Java Conference Speaker

Java Champion, author of the Javaspecialists Newsletter, conference speaking regular... About Heinz

Superpack '23

Superpack '23 Our entire Java Specialists Training in one huge bundle more...

Free Java Book

Dynamic Proxies in Java Book
Java Training

We deliver relevant courses, by top Java developers to produce more resourceful and efficient programmers within their organisations.

Java Consulting

We can help make your Java application run faster and trouble-shoot concurrency and performance bugs...